# AI Agent 协作指南 · zsjz-ai(清鉴) > 本文件是**仓库级**约定,供 AI 编码助手(WorkBuddy / Claude Code / Cursor / Copilot 等)在本仓库内改代码时遵守。 > > ⚠️ 别和下面两个文件搞混 —— 它们是**产品运行时的 AI 人格提示词**,不是给编码助手看的: > - `ai-server/src/main/resources/prompts/AGENTS.md` > - `QingJian/agent/AGENTS.md` > > 改那两个文件 = 改产品里 AI 研判专家的人设与话术;改本文件 = 改编码助手的行为约定。 --- ## 1. 项目一句话定位 **清鉴(QingJian)** —— 面向纪检监察 / 经侦机关的智能资金数据研判平台。 业务闭环: ``` 多源异构数据导入(话单 CDR / 银行账单 / 第三方支付 / 基站轨迹 / 外部库表) → 清洗治理(dm 模块) → 建模分析(通话·资金·轨迹·人物画像·图谱) → 可视化(ECharts / G6 关系图 / 离线栅格地图) → AI 对话式研判(AgentScope Agent + RAG + SQL 工具) ``` 领域术语(写注释/命名时沿用它,不要自创): `资金回流`、`闭环交易`、`快进快出`、`通联异常`、`时空伴随`、`白手套`、`攻守同盟`。 --- ## 2. 仓库结构 ``` zsjz-ai/ ├─ pom.xml # 父 POM(packaging=pom),Spring Boot 3.5.16,仅聚合 ai-server ├─ ai-server/ # 后端:Spring Boot 3 + MyBatis-Plus + AgentScope ├─ ai-frontend/ # 前端:Vue3 + Vite7 + TS + Ant Design Vue(JeeSite 脚手架血统) ├─ sql/ # 建表 / 初始化脚本(*.sql) ├─ QingJian/ # 运行期工作空间素材(agent 提示词、skills、conf、db、license) ├─ cmd/ lib/ pmtiles.exe # 本机工具(regInfo.exe 授权、mbtiles→pmtiles 转换等) └─ enc.md readme.md ``` `QingJian/` 不是源码目录,是**运行时数据目录**(含 `.db`、license 证书、导入模板),改动需谨慎。 --- ## 3. 技术栈 ### 后端(ai-server) | 项 | 值 | |---|---| | JDK | **25** | | 框架 | Spring Boot 3.5.16 | | 持久层 | MyBatis-Plus 3.5.17 + dynamic-datasource 4.5.0 | | 主库 | **PostgreSQL** + pgvector(master) | | 分析库 | **DuckDB**(slave,按工作空间切换) | | 缓存 | Redis | | 图库 | Neo4j(配置存在,非全链路强依赖) | | 鉴权 | Sa-Token 1.46 | | Agent | **AgentScope 2.0.1**(harness + dashscope/ollama/openai + mem0 + rag-simple + redis) | | 检索 | Elasticsearch 8.18(spring-boot-starter-data-elasticsearch,单索引+caseId 隔离) | | 其他 | fesod-sheet(Excel)、Hutool、Guava、MapStruct 1.6.3、commons-compress、JNA | | 启动类 | `com.zsjz.ai.App` | | 端口 | `8980` | > `ai-server/src/main/resources/app.yml` 是 **Solon 时代遗留配置,已不生效**,只作历史参考,不要照它改。 > 生效配置是 `application.yaml` + `application-dev.yaml`。 ### 前端(ai-frontend) - Vue 3.5 + TypeScript 5.9 + Vite 7 + **pnpm 10**(`preinstall: only-allow pnpm`,禁止 npm/yarn) - Ant Design Vue 4.2.6、Pinia 2.3、vue-i18n 11、UnoCSS 66 - 图可视化:**G6 5**、X6、`@relation-graph/vue`;图表 **ECharts 6** - 地图:**maplibre-gl + pmtiles**(离线栅格底图) - Markdown:vditor、streamdown-vue、markdown-it - Node 要求:`>=20.19 || >=22.12` **接口前缀 `/js`**,开发环境由 `.env.development` 代理到 `http://127.0.0.1:8980`: ``` VITE_PROXY = [["/js","http://127.0.0.1:8980/js",false]] VITE_GLOB_API_URL_PREFIX = /js VITE_GLOB_ADMIN_PATH = /a ``` 请求最终落到 **`/js/a/...`**(`urlPrefix(/js)` + `adminPath(/a)`), 与后端 `server.servlet.context-path: /js/a` 严格对应 —— 两者必须同步改。 调接口时用 `url: adminPath + '/xxx'`(`defHttp` 会自动补 `/js`); 不走 `defHttp` 的场景(`fetch` / ``)用 `glob.apiUrl + glob.urlPrefix + glob.adminPath + path`。 --- ## 4. 后端分层与落位约定 包根:`com.zsjz.ai` ``` com.zsjz.ai ├─ App.java 启动类 ├─ common/ 跨模块复用 │ ├─ config/ Result、WebConfig、MybatisPlusConfig、LicenseFilter、 │ │ DuckdbUnpooledDataSource、BigDecimal 序列化器 │ ├─ base/ Query、TreeNode、BasicColumn、Cleaned、ErrorEnum │ ├─ enums/ constants/ utils/ │ └─ model//{dto,entity,query,vo}/ ★ DTO / 实体统一放这里 └─ module// ├─ controller/ 仅收参 + 调 service ├─ service/ service/impl/ └─ mapper/ (XML 在 resources/mappers//*.xml) ``` `` 固定取值:`agent` `call` `dm` `external` `govern` `graph` `otg` `person` `plat` `track` `trans` 配套资源: ``` ai-server/src/main/resources/mappers//Mapper.xml ``` ### 命名规范 - 查询入参 → `common.model..query.XxxQuery` - 出参 DTO → `common.model..dto.XxxDTO` - 数据库实体 → `common.model..entity.Xxx` - Service 接口 `XxxService`,实现 `XxxServiceImpl` - 类名后缀:Controller / Service / Mapper / DTO / VO / Entity / Query / Enum / Const ### 编码规则 1. **Controller 不写业务逻辑**,只做参数校验 + 转调 service。 2. 分页统一用 MyBatis-Plus `Page`(`PaginationInnerInterceptor(DbType.DUCKDB)` 已注册)。 3. **统一返回 `Result`**:`Result.succeed(data)` / `Result.failure(msg)`。 > 存量 Controller(如 `CallNightController`)有直接返回裸对象的历史写法;**新增代码一律用 `Result`**,存量代码按需渐进改造。 4. 金额/比率用 `BigDecimal`(已配 `BigDecimalSerializer` / `BigDecimalDeserializer`),禁用 double。 5. 鉴权取当前用户:`StpUtil.getLoginIdAsLong()`。**会话、消息、模型配置等用户级资源必须带 userId 做归属校验**,不能只按 id 查。 6. 跨库访问用 `@DS("slave")`(参考 `SqlAnalysisTool` 依赖的 `SqlQueryMapper`);不标注则走 master。 7. 注释用中文,Javadoc 写清「做什么 + 为什么」,不写废话。 8. 新增枚举尽量实现 `HasInnerEnum`,与存量保持一致。 --- ## 5. 数据层要点 | 数据源 | 用途 | |---|---| | **master**(PostgreSQL + pgvector) | 平台元数据、`table_info`/`table_field`、Agent 会话/消息、模型与 Provider 配置、RAG 索引状态、向量检索 | | **slave**(DuckDB) | **按工作空间切换**的案情库;Agent 的 `execute_sql` 只在这里跑 | | Redis | 会话/缓存、Sa-Token、mem0 | | Neo4j | 图谱(配置在 `application-dev.yaml`) | - 表/字段元数据三表链路:`meta_raw_sheet` → `table_info` → `table_field` - 建表脚本放 `sql/`,新增表结构务必同步补脚本 **和** 元数据三表 - `mapUnderscoreToCamelCase` 已开启 ### ⚠️ 环境配置 `application-dev.yaml` 内含**本机数据库密码 / 内网 IP**,属于开发便利配置。 - 不要把真实凭证提交到公开仓库 - 不要在生产 profile 复用 dev 配置 --- ## 6. Agent 子系统(`module/agent`)★ 核心 ### 架构 AgentScope `HarnessAgent` + `Toolkit` + 中间件: ``` controller/ AgentChatController(REST + SSE,裸返回) AgentModelController(模型/厂商 CRUD,Result 包装) AgentResultController(execute_sql 结果集分页,裸返回) AgentPythonFileController(Python 产出图片文件服务) service/ AgentChatService、AgentModelService、AgentModelProviderService tools/ SqlAnalysisTool、RagSchemaSearchTool、PythonAnalysisTool、 GraphRenderTool、WorkspaceInfoTool intent/ 意图识别(IntentService / IntentMiddleware / IntentEnum) followup/ 追问生成(FollowupService / FollowupMiddleware) rag/ RagSchemaService、EmbeddingModelFactory、RagProperties python/ PythonExecutor(沙箱执行) sql/ SqlResultStore(结果集缓存,供前端分页;按登录用户归属) prompt/ PromptLoader、PromptHelper scaffold/ WorkspaceScaffolder(工作空间初始化) mapper/ Agent*Mapper、SqlQueryMapper、RagVectorMapper entity/ AgentChatSession、AgentMessage、AgentModel、AgentModelProvider、RagIndexStatus ``` ### 工具清单(`@Tool` 注解) | 工具 | 说明 | |---|---| | `execute_sql` | 在当前工作空间 DuckDB 上执行**只读** SQL,返回 resultId + 列定义 + 当前页 + 总行数 | | `list_tables` | 列出工作空间全部表(中英文名、字段类型与中文注释) | | `search_table_schema` | 基于 pgvector 的表结构语义检索 | | `render_chart` | 校验并返回 ECharts 图表配置(**禁止**用于关系图) | | `render_graph` | 校验并返回力导向关系图谱 JSON —— 关系图**只能**走这里 | | `execute_python` | 在沙箱中执行 Python 脚本分析,产出图片经 `/py/files/**` 访问 | | `get_current_workspace` / `get_user_selected_workspace` | 读取当前/用户所选工作空间信息 | | `get_call_records` | 通话原始明细(`CallRecordService#getCallRecord`) | | `stat_call_night_summary` / `stat_call_night_detail` | 夜间通话一/二层(`CallNightService`) | | `stat_call_continuous_summary` / `stat_call_continuous_detail` | 持续联系一/二层(`CallContinuousService`) | | `stat_call_sensitive_summary` / `stat_call_sensitive_detail` | 敏感日期联系一/二层(`CallSensitiveService`) | #### 业务域工具分组(`AgentToolRegistry`,当前实际注册 67 个) 只读 service 由 `AgentToolRegistry` 统一注册为 **6 个工具组**,默认只装备 `person` 组, 其余组由模型调用 `reset_equipped_tools` 按需切换 —— 避免每轮把全部 JSON Schema 塞进上下文。 | 组名 | 数量 | 覆盖能力 | 工具类 / 入参类 | |---|---|---|---| | `person`(**默认装备**) | 24 | 人员枚举与分组、通话/交易画像、常联系/常交易 TOP、汇总、运营商归属、银行卡、交易趋势、9 类亲密度行为 | `PersonAnalysisTool` / `PersonToolSpecs` | | `call` | 7 | 通话明细、夜间/连续/敏感通话的汇总与明细 | `CallAnalysisTool` / `CallToolSpecs` | | `trans` | 16 | 交易明细、大额、共同/相互交易、代持卡、现金流、连续交易、快速资金流转、理财、定期存款、频次、敏感交易 | `TransAnalysisTool` / `TransToolSpecs` | | `track` | 16 | 基站轨迹、境外本地昼夜活动、快递、碰面、同住、同行出行 | `TrackAnalysisTool` / `TrackToolSpecs` | | `graph` | 1 | 关系图谱取数(`get_case_graph`) | `GraphAnalysisTool` / `GraphToolSpecs` | | `file` | 3 | 非表格材料档案:清单检索、分类统计、读正文样本 | `FileAnalysisTool` / `FileToolSpecs` | > ⚠️ 上表是**当前实际值**,不是设计稿:`otg`(全文检索,`OtgAnalysisTool`)**未注册** > (`OTG_TOOLS` 为空、`createGroup` 整段注释),`trans` 的 `TRANS_TOOLS` 清单也还没补全, > `graph` 的明细类工具同样没实现。改动工具清单时同步更新 > `AgentToolRegistryTest#TOTAL_BUSINESS_TOOLS` 与 `AgentScopeMcpToolProviderTest` 的数量下限。 > 分组机制要点(`AgentToolRegistry` 注释里有完整说明): > - 组必须是 `ToolGroupScope.META`,否则 `reset_equipped_tools` 会拒绝("not manageable by this tool") > 且组名不会出现在该工具 `to_activate` 的 enum 里; > - 未激活组里的工具**不进** `getToolSchemas(activeGroups)`,模型完全看不到; > - 新会话的初始激活组 = `ReActAgent` 在 build 时捕获的 `toolkit.getActiveGroups()`, > 所以「默认装备哪一组」只由 `AgentToolRegistry#DEFAULT_ACTIVE_GROUPS` 决定; > - `reset_equipped_tools` 是**全量替换**语义(不在 `to_activate` 里的组一律卸载), > `AgentService#appendRenderPrompt` 里已提醒模型「把还要用的组一并列出,用完切回」。 > - 已知限制:`ReActAgent` 在会话恢复/结束时用共享 `Toolkit` 的全局 activeGroups 做 `set/get`, > 同一 agent 实例的并发会话理论上可能互相覆盖全局状态(每轮实际按会话状态过滤,影响有限)。 > 通话分析 7 件套由 `CallAnalysisTool` 包装 `module/call` 的既有 service,**只做适配不改业务**, > 保证「页面上的结论」与「AI 的结论」同口径;返回形态与 `execute_sql` 一致 > (`resultId` + `columns`/`rows`/`totalRows`,前端 `DataTableBlock` 直接渲染可翻页表格), > 单次取数上限 **10,000 行**(比 `execute_sql` 收紧,因 `SqlResultStore` 是 LRU 50 的共享缓存), > 超限返回可读报错而不是静默截断。入参是专用 POJO(`CallToolSpecs`), > **不暴露** `orderKey`/`sort`/`tableName`(Mapper XML 里是 `${}` 拼接,属注入面)。 > 其余 5 个域同构:入参 `*ToolSpecs`、出参 `ToolResultTable`、上限 10,000 行。 > > **唯一例外**:`get_case_graph` 返回 `{nodeList, edgeList}` 结构化 JSON(不表格化,因为 > `render_graph` 要的就是这个结构),且边数超过 500 时按权重降序截断并附 > `truncated` / `totalEdges` / `note`,防止 1 万条边的 JSON(约 30 万 token)冲爆上下文。 ### 硬约束(不要放宽) - 结果集上限 **100,000 行**,超出要求模型加 `LIMIT` / `WHERE` - `page_size` 默认 50,上限 500 - 单元格字符串截断 **500** 字符 - 只读白名单首词:`select / with / pragma / describe / show / explain`,**禁止多语句(含分号)** - `render_chart` 会硬拦截 `series.type == "graph"`,强制改走 `render_graph` ### SSE 流式协议 `POST /chat/stream`,事件按序:`tool_call` → `tool_input` → `tool_result` → `token`(多次) → `done` / `error` payload 结构: ```ts { type, data?, toolCallId?, toolName?, toolInput?, toolResult?, error?, sessionKey? } ``` 前端消费方:`ai-frontend/src/ai/api/`(`chatApi.ts` / `types.ts`)。**改动协议必须前后端同步**。 ### 提示词 - 运行时系统提示词:`ai-server/src/main/resources/prompts/AGENTS.md` - 未经明确要求,**不要重写人格设定与输出格式**(【异常概览】/【资金链路分析】/【通联行为画像】/【时空碰撞结果】/【研判结论与建议】),那是产品行为的一部分。 ### 前端渲染管线(`ai-frontend/src/ai/`) 后端 `AgentService#appendRenderPrompt` 强制的输出契约 → 前端块的映射: | AI 输出形式 | 前端块 | 组件 | |---|---|---| | **裸 JSON 混在 markdown 正文**(`execute_sql` 结果,**禁止围栏**) | `table` | `blocks/DataTableBlock.vue`(按 resultId 调 `/chat/results/{id}` 翻页) | | ` ```echarts ` 围栏(ECharts option JSON) | `echarts` | `blocks/EChartsBlock.vue` | | ` ```graph ` 围栏(`{title,nodes,edges,categories}`) | `graph` | `blocks/GraphBlock.vue`(`@relation-graph/vue`) | | ` ```html ` 围栏 / 裸 ``·`` | `html` | `blocks/HtmlBlock.vue`(sandbox iframe) | | 其余代码围栏 / markdown 正文 | `code` / `markdown` | `blocks/CodeBlock.vue` / `blocks/MarkdownBlock.vue` | **关键实现约束**:因为表格是裸 JSON 而非围栏,`utils/scanner.ts` 必须做**字符级花括号配平扫描** (字符串/转义感知)并按载荷形态校验,不能只按围栏切分。流式期间的增量策略是 「已完成块冻结(尾部 8KB 窗口外)+ 只重扫尾部 + 尾部块按 key 复用对象」, 块 key 用 `kind:start`(内容只追加 → key 稳定,避免图表实例重建、表格分页被重置)。 改渲染契约(`appendRenderPrompt`)时必须同步改前端 `scanner.ts` 与对应块组件。 --- ## 7. 数据清洗 / 治理(`module/dm`) ``` dm/ ├─ pre/ PreTask、PreDataListener、DateNumberConverter、Str2NullConverter ├─ clean/ │ ├─ DataCleaner(接口)、CleanResult、CleanCache、DirtyDataHandler、StrategyType │ ├─ impl/cleaner/ 各数据源清洗器(Call*/Trans*/Track*/External*/...) │ ├─ impl/loader/ 数据装载器 │ └─ func/ 可复用清洗函数:FunAbs/FunRadix/FunReplace/FunTime/FunMultiplier/... ├─ ai/ 非表格文件的 Tika 抽样本 + 大模型识别 │ (FileSampleExtractor / FileRecognitionService / FileAiProfileService) └─ mapper/ CleanErrorLogMapper、DmMapper、PersonRelEdgeMapper、EdgeConfMapper、 FileInfoMapper、FileAiProfileMapper ``` - 新增一种数据源导入 = 新增一对 `Loader` + `Cleaner`(命名对齐 `XxxDataLoader` / `XxxDataCleaner`) - 清洗错误必须落 `CleanErrorLog`,不要静默吞掉 - 可复用变换优先写成 `func/Fun*` 而不是在 Cleaner 里硬编码 - **非 xls / xlsx / csv 的文件不落盘**:只登记 `file_info`(`FILE_UNSUPPORTED_FAIL`)+ `file_ai_profile`(Tika 文本样本 + 大模型识别的分类/摘要/关键词,供后续数据分析工具按 `category` 分流)。 识别链路 3 路并发、其余排队(`zsjz.file-ai.*` 可调),**全程 fail-open** —— 抽样本失败/模型报错只落 SKIP/FAIL,绝不能让上传接口失败。抽样本必须在**请求线程内**做 (multipart 临时文件在请求结束被清理)。详见 `ai-server/src/main/java/com/zsjz/ai/module/dm/ai/` - **`file_ai_profile` 带 `caseId`(绑定到案件上)**:建档时从 `CaseContextHolder` 读一次, 写进档案,同时把同一个值传给 `FileRecognitionService#submit` —— 分两次读万一中间上下文变了, 会落出「档案说 A 案、识别写回 B 案」的脏数据。**查询不按 caseId 过滤**: 案件范围由 `CaseRoutingDataSource` 按上下文自动限定,再叠一层只会制造「两边不一致就查不到」的假故障。 - **改 `file_ai_profile` 的列要动 4 处**:实体 `FileAiProfile`、`CaseDataSourceRegistry#AI_PROFILE_DDL`(新库)、 `CaseDataSourceRegistry#AI_PROFILE_MIGRATIONS`(老库补列)、`sql/case_table_1.sql`。 ★ **`CREATE TABLE IF NOT EXISTS` 对已存在的表是空操作** —— 老案件库里表已建好, 光改建表语句**永远加不上新列**,表现是「新环境好好的,老案件一识别就报 Column not found」。 所以新列必须同时写进建表语句和补列清单。`FileAiProfileSchemaTest` 会按生产顺序跑 DDL + 补列并比对实体注解,漏改一处就红;另有 `legacyTableGetsCaseIdColumn` 专门模拟 「已有旧版表的老库」验证补列路径。 - **AI 识别只认文本类文件(后缀白名单)**:唯一事实来源是 `FileTypeConstants.AI_RECOGNIZABLE_SUFFIXES`,判定走 `FileTypeConstants.isAiRecognizable(fileType)`。 白名单 ≈ pdf / doc / docx / ppt / pptx / rtf / odt / ods / odp / wps / wpt / dps / dpt / eml / txt / text / md / markdown / log / xml / json / html / htm / sql / tsv / yaml / yml / properties / ini / conf / cfg。 压缩包、图片、音视频、可执行文件、数据库文件、**无后缀文件(`fileType = "unknown"`)一律不识别**。 ★ 三条硬约定:**用白名单不用黑名单**(后缀无穷无尽,黑名单漏一个 = 默认识别,白名单漏一个只是 「该识别的没识别」,代价可控);**`csv / xls / xlsx` 绝不加进白名单**(它们走表格解析流水线, 根本到不了这条路);**守卫放在 `FileAiProfileService#register` 入口**,不是调用方 `DmService` —— 绕过是静默的(只是多花钱、识别不出东西),不报错就发现不了,只有放在唯一入口才拦得住后续新增的调用点。 加后缀时同步改 `FileTypeConstantsTest`(`whitelistEntriesAreNormalized` 会拦住「大写」和「带点号」的条目) - 同一批档案还给 AI 用:`file` 工具组(`FileAnalysisTool`)提供 `list_file_profiles`(清单检索)/ `stat_file_profiles_by_category`(分类统计)/ `get_file_sample`(读某份材料的正文样本,≤4000 字)。 ★ 三条硬约定:**正文绝不进列表**(单条样本 4000 字,50 行就是 20 万字,会冲爆上下文, 正文只能按需单份取);**命中超 5000 条报错而非截断**(截断会让模型以为材料就这么多); **分类/状态认不出必须报错**(`FileCategoryEnum.parse` 认不出会落 `OTHER`, 直接用它会让「过滤笔录」静默变成「过滤其他」)。 --- ## 8. 前端约定 目录按业务域划分,与后端 `` 基本对齐: ``` ai-frontend/src/ ├─ core/ 脚手架通用层:components/、layouts/、api/、hooks/、store/、router ├─ ai/ AI 数据分析应用(api/、utils/、store/、styles/、components/、views/) ├─ case/ 案件:数据导入/清洗/治理/人物画像/线索推送 ├─ call/ 话单分析(night、continuous、sensitive...) ├─ trans/ 资金分析 ├─ track/ 轨迹分析 ├─ person/ 人员 ├─ plat/ 平台/模板 ├─ dm/ 清洗类型定义 └─ graph/ 图谱(G6 编辑器、力导向布局 worker、资金流向图) ``` 每个业务域内:`api/`(请求 + `types/dto.ts`、`types/query.ts`)、`views/`、`components/`、`hooks/`、`types/`。 ### 规则 1. **优先复用 `src/core/components/*`**(BasicTable / BasicForm / BasicModal / Description / AnalysisPagination 等),不要新造轮子。 2. 组件用 `