# xlyAi Agent 目标架构 > 状态:设计已收敛(2026-07-21)。全 Java / LangChain4j,单 ReAct agent + 通用工具 + 人在环写入。 > 关联:知识图谱底座见 `sql/README.md`;现状代码接线见 memory `xlyai-rag-milvus-wiring`。 ## 1. 背景与目标 把 xlyAi 从「元数据驱动的多智能体(8 场景 + 每表单一个 action 工具)」重构成: **单个 ReAct agent + 少量通用工具 + 人在环(HITL)写入**。agent 自己判断调工具,不再按场景路由。 **关键决策(含取舍)** - **全 Java / LangChain4j,不引入 langgraph4j、不换 Python/JS。** 代价:没有 LangGraph 的 checkpoint/interrupt/subgraph,HITL 与多步编排要手搓(用 immediate-return + ChatMemory + Redis,够应付请求-响应式对话 + 单步确认;复杂长事务将来会更费劲)。 - **观测**用自托管 **Langfuse**(不用 LangSmith,避免 ERP 数据出网)。 - **写入永远人在环双确认**,LLM 绝不直写业务表。 ## 2. 拓扑与数据流 ``` 浏览器前端 xlyAi (Java/LangChain4j :8099) saas ERP 后端 xlyEntry(:8697) ├ 聊天 UI(流式/diff确认/QA选项) ⇄ ┌ 单 ReAct Agent (AiServices 工具循环) ⇄ ├ 表单读写 API (getBusinessData…) └ ERP 原生表单(预填/执行确认) │ ChatMemory(Redis) · SystemPrompt(L1) ├ 暂存执行器 (新建, 排空 sp_ai_addcommonwx) │ 5 通用工具 + Skill 注册表 └ MySQL (业务库) └ 直连: Ollama(qwen2.5)/Milvus/MySQL(KG+NL2SQL) ``` **数据流要点(重要)** - **Read / Write 的业务逻辑在 ERP 后端**,不在 xlyAi。xlyAi 的工具 = **薄 HTTP 客户端**,发和前端一样的 API 请求(`erp.baseurl` + `getBusinessDataByFormcustomId`)。 - **Query / NL2SQL 例外:xlyAi 直连 MySQL**(`DynamicExeDbService`),不经 ERP 后端 → 这是它击穿权限的根源,需单独锁(见 §7、§9)。 ## 3. Agent 核心 - **单 agent** = LangChain4j `AiServices` 原生 **tool-calling 循环**当 ReAct(不用老式文本 Thought/Action;qwen2.5 支持 function calling)。 - **退役** 8 场景 `SceneSelector` 路由 —— agent 自路由。 - **System prompt** = 角色 + **L1 业务域图(`viw_kg_domain`, 11 行)** + Skill 摘要列表 + 工具用法 + 安全约束。 - **记忆** = `ChatMemory`(窗口化 + 超长摘要),持久化到 **Redis**(键:`chat:{userId}:{conversationId}`,带 TTL)。 - **循环护栏**:最大工具迭代步数、单轮墙钟超时、单轮 LLM 调用上限。 ## 4. HITL 机制(无 LangGraph 的中断/续跑) 用 LangChain4j 的 **immediate-return tool**(现有 `immediateReturnToolNames` 机制):这类工具一被调用就把结果**直接返回前端并结束本轮**,不回喂 LLM。 - **AskUser / ProposeWrite / 表单呈现** 都是 immediate-return。 - **续跑**:本轮结束后,会话置 `pendingInteraction`(如 `awaiting_answer:{id}` / `awaiting_confirm:{stagingRef}`)。用户的下一条输入按此状态被路由回「上一步的答复」,而非当作新请求。**没有这个 pending 状态机,多轮 HITL 会错乱** —— 这是手搓 HITL 的关键点。 - **确认动作走确定性端点,不过 LLM**:diff 的「确认」按钮 → 直接打 xlyAi 一个 endpoint 落暂存 + 返回 ERP 链接。 **前端事件协议(SSE)**:`token`(流式文本)/ `tool_call`(可选可视化)/ `question{text,options[]}` / `write_proposal{diff,stagingRef}` / `link{url}` / `error` / `done`。 ## 5. 工具规格(5 通用工具 + KG 预留) > 通用原则:入参/出参 JSON-schema、无状态、**auth 走 per-call context**(MCP 形状,便于日后暴露成 MCP)。输出**必须分页/截断**,绝不吐爆上下文。 | # | 工具 | 职责 | 关键入参 | 出参 | 安全 | HITL | |---|------|------|----------|------|------|------| | 1 | **Read** | 调**任意 ERP 表单/过程**(明细 **+ 预建聚合** AR/AP/库存/进度,即旧 type-5),包 `getBusinessDataByFormcustomId` | formId, moduleId, **白名单过滤**, page | 行(截断)+分页游标 | 租户+行级;**表单权限靠 xlyAi 授权层**(§7);参数白名单 | 否 | | 2 | **Query** | 只读 SQL 沙箱,**仅 ad-hoc 兜底**(没有现成表单/过程能答时才用) | 自然语言 → SQL | 聚合结果(小) | ⚠ 见 §9 | 否 | | 3 | **KgSearch** | L2 邻居 / L3 字段→列→表 / 解析 formId(**暂缓,预留接口**) | 表单名/术语/域 | 子图/映射 | 只读 | 否 | | 4 | **AskUser** | 小问题(选项 + 自由输入),消歧/澄清 | question, options[] | — (immediate-return) | — | ✅ | | 5 | **FormCollect(表单呈现)** | 渲染一张表单收集 N 个参数(字段/类型/下拉来自 ERP 元数据),用户填 | formId(+ 已知预填) | 收集到的参数 | — | ✅ | | 6 | **ProposeWrite** | 出 diff → 确认 → 暂存(单一写工具) | formId, op, payload | diff + stagingRef | 见 §12 | ✅ | - **路由原则**:预建表单/过程能答的 → **Read(安全)**;纯 ad-hoc 分析 → **Query(锁死)**。压缩 Query 危险面。 - **Read/ProposeWrite 的 formId 定位**依赖 KgSearch(过渡期先用表单名模糊匹配 `viw_ai_useful_forms`)。 - **「取表单 schema」能力**(字段/类型/必填/下拉/默认,数据在 `gdsconfigformslave`)供 **FormCollect 渲染** 和 ProposeWrite 预填。复杂写(报价 25 字段)= FormCollect 渲染该表单让用户填,**无需 Skill**。 ## 6. Skills(自搭轻量版,框架无原生支持) - Skill = 针对重复任务的 **playbook**(如 月度对账 / 新建报价 / 超期应收催收),**用工具但不是工具**。 - 注册表:`{name, 何时用(1行), 详细指令, 建议表单/工具}`。 - 渐进披露:**name + 何时用** 进 system prompt(像 L1 一样便宜);`load_skill(name)` 工具按需注入完整指令。 - 接替旧场景人设的「领域指导」,但变成**可加载模块**(与取消多智能体一致)。 ## 7. 鉴权与授权模型 **凭证透传(不变)**:用户 token 登录 ERP 后存浏览器;前端每请求带给 xlyAi → xlyAi 工具转发进 ERP API 的 `Authorization` 头;只在单次请求上下文临时持有,不长期存;**token 绝不进 prompt / LLM 可见文本**;基础设施凭证(Ollama/Milvus/DB 池)与用户凭证分离。 **⚠️ 授权:后端不是权限权威(2026-07-21 调查证实)** - ERP 后端 `getBusinessDataByFormcustomId` + **所有写/动作端点(add/update/delete/审核 doExamine)只验登录 `@Authorization`**;逐用户表单/菜单权限 `checkByUser` **被故意注释**(`// 朱总说不用放 20230626`)。 - 后端**只强制**:公司级租户隔离(`sBrandsId/sSubsidiaryId`)+ 行级 `jurisdiction` 数据范围。 - 用户的表单/菜单权限是 **UI-only**(前端按 `sAuthsId` 过滤菜单)。**agent 直接打 API 绕过 UI → 能碰用户界面里看不到的表单** = 越权放大。 - 读 API 还能被写:通用 API 把请求体任意 key 按名绑定到过程 IN 参、无白名单 → `SP_Inventory_InOutWarehouse`(材料库存台账,AI 已暴露)在 `bUpdate=1` 时真改库存表。 **ERP 授权本有三层,实际只跑两层**:租户隔离 ✅ / 行级 jurisdiction ✅ / **表单-菜单级权限(`sAuthsId`)❌ 被注释、仅前端 UI 用**。第三层的**数据仍在**(`sAuthsId` 记录每用户授权的表单)——所以可在 agent 侧补回。 **⇒ 新增架构组件:xlyAi 侧授权层 = 表单级白名单。** 读 `sAuthsId` 把 agent 限制在用户**实际有权的表单/动作**集内,**Read/Query/Invoke 共用这同一个边界**。**只做表单级、不做字段级**(表单内所有列可见——2026-07-21 决定)。方案 **B(xlyAi 强制,推荐)** vs A(重开后端 `checkByUser`)vs C(两者)。 - **Read 参数白名单**:只传已知过滤/分页参数,**绝不透传 `bUpdate/bUpdateAll` 等**(否则读变写)。 - **Query 例外**:直连 DB 无任何后端兜底 → xlyAi 强制注入租户 + 视图白名单(见 §9)。 - **Invoke/写动作**:后端零权限校验 → 必须 xlyAi 授权 + 走 ProposeWrite 确认门,**绝无绕过确认门直接触发 ERP 写动作的路**。 ## 8. 上下文管理 单 agent;L1 进 prompt 做路由;KG 收窄「该看哪些表」;工具输出分页/摘要;ChatMemory 窗口化 + 超长摘要存 Redis;工具循环步数/超时护栏。 ## 9. 安全 - **授权(新,见 §7)**:ERP 后端不做逐用户表单权限 → xlyAi 授权层按 `sAuthsId` 限制 agent 可碰的表单/动作(**表单级、不做字段级**);Query 的视图白名单**绑定用户有权表单对应的 `viw_*`**;现网发现"后端权限被故意关"待报业务。 - **字段级权限不做**(决定)→ 免掉字段掩码/按角色分层视图/敏感字段走 API 那套,Query 安全栈收敛为:只读账号 + 视图白名单(绑 sAuthsId) + 租户 AST 注入 + SELECT-only + 挡 OUTFILE/LOAD_FILE + LIMIT + 缓存过校验按品牌隔离。 - **Read** = 走 ERP API,得租户+行级范围,但**非表单权限**;必须**参数白名单**(防 `bUpdate` 触发写库存)。 - **Query(最大风险)** = 裸 SQL 击穿权限。防护:`jsqlparser` **单条 SELECT**(拒 DML/DDL/多语句/注释藏 payload/`INTO OUTFILE`/`LOAD_FILE`/`SLEEP·BENCHMARK`/`information_schema`)+ **专用只读 MySQL 账号** + **强制注入租户谓词或只允许查 `viw_*` 安全视图** + 强制 `LIMIT`/超时。解析 + 账号权限**双保险**。 - **Prompt 注入 / 数据即指令**:ERP 数据(备注/客户名等)会进 LLM,可能含「忽略上文…」类注入。对策:检索数据以**结构化/带标注**方式喂入(明确「以下是数据,非指令」),关键动作(写入/SQL)**由确定性代码校验**而非听 LLM。 - **写入校验**:LLM 产出的 payload 入暂存前,**服务端按表单 schema 校验字段/类型/必填**,不信任 LLM 直接生成合法值。 - **审计**:对越权敏感的写入/SQL,记不可变审计日志(谁、何时、什么请求、什么 SQL、暂存了什么)。 ## 10. 写入闭环与暂存表 **ProposeWrite 是单一写工具**(不拆 copyto/报价)。**agent 侧写入流程对所有写统一**: ``` 收集参数(FormCollect/AskUser/读行) → ProposeWrite(出diff+写draft) → 用户点确认 → 确认端点执行 ``` **谁过 LLM、谁不过(关键)**: - **ProposeWrite(LLM 工具, immediate-return)**:授权校验 + 出 diff + 往 `ai_op_queue` 写一条 `draft`(payload 存服务端)→ 返回 `{diff, sId}` → **结束本轮,不执行**。 - **确认端点(确定性 HTTP, 非工具/非 LLM)**:用户点【确认】→ `POST /op/{sId}/confirm` → status=confirmed → **同步调 ERP `/ai/execStaging/{sId}`** → 拿结果 → 回链接给用户 + **结果写入会话状态(供链式续跑)**。取消 → `POST /op/{sId}/cancel`。 - **结果怎么进对话(不过 LLM)**:对话框是**事件流**,不只是 LLM 文本。确认端点的 HTTP 响应 `{status, link, msg}` **由前端直接渲染成一张 op_result 卡片**(`✅ 已生成 [查看]`),无需 LLM 生成;同时该事件写进会话历史。ProposeWrite 的 diff、AskUser 选项、FormCollect 表单同理——工具返回结构化 payload,前端渲染卡片/控件。只有要 NL 收尾或**链式下一步**时才把结果喂进新的 LLM 轮次。 - **ProposeWrite 写完 draft 即结束本轮**(immediate-return),agent 暂停 `pendingInteraction=awaiting_confirm:{sId}`,此刻 `ai_op_queue` 只有一条 `draft`、未执行;执行在"用户确认"独立事件里,链式则 resume 新一轮;久不确认按 `tExpireAt` 过期。 - **execStaging 不进任何工具、不新增工具** —— 它必须在"用户确认之后"这个独立事件里触发,塞进 immediate-return 的 ProposeWrite 会变成"确认前就执行"。工具仍是 6 个。 - 报价即使已用 FormCollect 填完表单,**落暂存前仍要一份对话内最终确认**(确认 = 允许存起来的门)。 - 落暂存后是 **ERP 的事、与 agent 无关**:ERP 对不同待办的策略(报价自动执行 `Sp_Ai_AddQuoQuoAfter` / copyto 让用户在 ERP 原生表单再确认→执行器执行)都在暂存之后。 - **ProposeWrite 泛化为"提议任意变更动作"**:ERP 按钮动作(审核/过账/生成 等**写按钮**)也走这道确认门;只读按钮(查询/导出)归 Read。⚠️ 绝不给 agent 一条绕过确认门直接触发 ERP 写动作的路(后端权限只管"能不能做",不等于"这次同意做")。是否需要独立的"按钮调用"底层能力,待后台调查(过程是否写、读 API 会否触发写、后端是否对表单/过程/动作调用鉴权)后定。 **通用双确认流(copyto)**:① 对话内 diff + 确认按钮 → ② `ProposeWrite` 落暂存 `sp_ai_addcommonwx` + 返回 ERP 链接 → ③ 用户进 ERP 原生表单(预填)再确认 → ④ **ERP 暂存执行器**排空并执行真实写入。 - **现状缺口**:现有 `sp_ai_addcommonwx`(239 行)**完全只为"生成下游单据"设计**(字段全是 源→目标 + 合并/分单模式),且**没有消费者**(`Sp_Ai_AddCommonAfterNew` 只入队从不执行)。缺 op 类型/目标记录id/payload/**用户·租户**/status/结果/自动标志/过期 → **覆盖不了 增/改/删/审核**。 **⇒ 新建通用暂存表 `ai_op_queue`**(旧表留给存量或迁移): ``` ai_op_queue( sId PK/深链token, sUserId, sBrandsId, sSubsidiaryId, sOpType, -- create|update|delete|examine|generate sTargetFormId, sTargetModuleId, sTargetTable, sTargetBillId, -- update/delete/examine 用; create/generate 空 sPayload JSON, -- 列→值(create/update) 或 动作参数(examine) sSourceRef JSON, -- generate: 源表单+选中 sSlaveId 集 bAutoExecute, -- ERP 自动执行 or 等用户提交 sStatus, -- draft|confirmed|executing|executed|failed|expired sResultBillId, sResultMsg, sErrorMsg, tCreateDate, tConfirmDate, tExecutedDate, tExpireAt) ``` `sOpType + sTargetBillId + sPayload` 统一覆盖所有写。报价=generate/quote+auto;审核=examine+目标id;改删=update/delete+目标id+payload。 **卡片三态状态机**:`处理中` / `待处理`(manual 终态) / `已完成·失败(重试)`(auto 终态)。用户点确认 → `/op/{sId}/confirm` → 卡片=**处理中**(不阻塞聊天,LLM 已停,仅此卡片转圈)。前端从响应(快=同步)或 SSE 推送(慢=异步)拿终态渲染,不过 LLM。 **auto 流程**(报价):confirm → ERP 检查暂存并**在一个事务里执行**该行 → 回响应含 `billId+link`(或 `failed+errorMsg`)→ 卡片渲染 `✅已生成[查看]` 或 `❌失败[重试]`,结果写会话。链接打开=**已建好的单据**。 - ⚠️ **原子**:execStaging 必须事务化,半途失败**自动回滚**,重试才安全。 - ⚠️ **幂等**:重试仅当 `status=failed`;`sId` 作幂等键,防"执行成功但响应丢失→重试→重复建单"。 **manual 流程**(新增/改/删/审核):confirm → ERP 只**校验暂存存在且有效**(失败/过期→失败卡片)→ 回 `link`(不执行)→ 卡片渲染 `待处理[去ERP操作]`。**xlyAi 到此终止,此卡片不再更新、ERP 不再回响应。** 链接打开=**字段已预填/预改的原生单据**,用户手动点 保存/审核/作废 才真正落库。 - ⚠️ **代价**:xlyAi 拿不到最终单据 id、不知用户是否完成 → **跨 manual 写不能链式**(终态交接)。auto 写可链式。将来要 manual 链式需 ERP 完成回调。 - **两种链接**:`bAutoExecute=1`(报价)→ ERP 立即执行 → 链接 `/form/{formId}?bill={sResultBillId}` **打开建好的单据**;`bAutoExecute=0`(新增/大多数)→ ERP 只校验+备预填 → 链接 `/form/{formId}?staging={sId}` **打开预填表单,用户点提交/保存**(走 addBusinessData 才真正写)。 - **ERP 侧要新建**:① 预填加载 `/form/{formId}?staging={sId}`;② 执行端点 `/ai/execStaging/{sId}`;③ status/result 回写;④「我的 AI 待办」入口。 - **需处理**:幂等(重复确认)、过期、确认前底层数据变化、状态机、失败回执、审计。 ## 11. KG 集成 - **L1** `viw_kg_domain` → 渲染进 system prompt(路由地图)。 - **L2/L3** `viw_kg_edge_flow`/`viw_kg_form_neighbors`/`viw_kg_edge_ref`/`viw_kg_field_dict` → `KgSearch` 工具按需查(**L3 必须按词查询,不整表 dump**)。 - 表单卡片 `viw_kg_form_card` → 可选向量召回(先本地内存或 SQL 匹配,远端 Milvus 后置)。 ## 12. 复用 / 退役 / 新建 | | 内容 | |---|---| | **复用** | `AiServices` 循环、`ChatMemory`/`OperableChatMemoryProvider`、`VectorizationService`、表单 API 客户端、NL2SQL 护栏 + `ai_global_agent_question_sql` 缓存 + `ai_sql_error_history` 纠错、`jsqlparser`、Milvus/embedding | | **退役** | 8 场景 `SceneSelector`、`DynamicToolProvider` per-form `ToolMeta` 工具、手写 `explainMilvusResult` RAG(并入工具) | | **新建** | 5 通用工具、取表单 schema 能力、Skill 注册表 + `load_skill`、Redis 会话持久化 + `pendingInteraction` 状态机、L1 prompt 注入、diff 确认端点、**ERP 侧暂存执行器 + 预填 + 深链 + 待办入口**、审计日志、Langfuse 接入 | ## 13. 迁移路线(strangler,不大爆炸) - **P0 KG 底座** ✅(`sql/` 7 视图,分支 `kg-edge-flow`)。 - **P1 单 agent 骨架 + Read + Query(只读优先、安全先行)** —— 灰度一个只读场景,用现有 62 工具当**回归基线**。 - **P2 AskUser(QA) + KgSearch + Skill 注册表**。 - **P3 ProposeWrite + ERP 暂存执行器 + 预填/深链**(最重、跨团队,先排脚手架)。 - **P4 退役旧多智能体/元数据工具;接 Langfuse;报价统一走确认制**。 ## 14. 已定 / 开放问题(见对话讨论) **已定(2026-07-21)** - ✅ **写入需链式 → 必须跨会话状态**:写操作不是一次性。skill 可能"下单→据此生成入库",第二步依赖第一步**已执行**。⇒ 需 ERP 执行后**回调**通知 agent + **跨会话/长任务状态**(会话可在 ERP 执行完后 resume)。这超出单会话 ChatMemory,需要一个持久的「任务/流程实例」存储(Redis/DB)。 - ✅ **审计强制留痕**:金融类数据,记不可变审计(谁、何时、什么请求、什么 SQL、暂存/执行了什么)。审计独立于 Langfuse(LLM tracing ≠ 业务审计)。 **开放** 1. **Query 越权锁法**:仅 `viw_*` vs 注入租户谓词 vs 复制 ERP 的表单/字段级 ACL —— **待现有 NL2SQL 安全调查结论后再定**(正在后台查:现状是否有租户过滤、是否会越级)。 2. 部署形态:单品牌一实例 vs 多品牌共库(决定 KG/缓存/Query 是否按 `sBrandsId` 切分)。 3. ERP 侧暂存执行器 + 预填 + 深链 + 执行回调归谁做、能否排期。 4. 模型:qwen2.5:14b 多工具 tool-calling 可靠性(否则换模型 / 加 tool-call 参数校验兜底)。 5. 多语言:仅中文 vs 支持 en/big5。 6. 报价改双确认(失去即时提交)需业务确认。 7. HITL `pendingInteraction` + 跨会话任务状态语义 + 前端事件协议,与前端团队对齐。 8. KG 卡片召回:本地内存向量 vs 远端 Milvus 时机。 9. 首个灰度只读场景选哪个。