agent-architecture.md 35.7 KB

xlyAi Agent 目标架构

状态:设计已收敛(2026-07-21)。全 Java / LangChain4j,单 ReAct agent + 通用工具 + 人在环写入。 关联:知识图谱底座见 sql/README.md;现状代码接线见 memory xlyai-rag-milvus-wiring

阅读指引:§1–§14 是设计(决策与取舍,基本稳定);§15/§16 是历史实现快照(按日期,不再更新); 当前实现状态看 §17,未完成清单看 §18。分支 kg-edge-flow(尚未合并 master)。

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选项)  ⇄ ┌ 意图门(受约束JSON) → 按意图收窄工具集    ⇄  ├ 表单读写 API (getBusinessData…)
└ ERP 原生表单(预填/执行确认)      ├ 单 ReAct Agent (AiServices 工具循环)      ├ 暂存执行器 (/ai/execStaging)
                                  │ ChatMemory(Redis) · SystemPrompt(L1)      └ MySQL (业务库)
                                  │ 9 个工具(6 类) + Skill 注册表
                                  └ 直连: Ollama(qwen3:14b)/MySQL(KG+NL2SQL)(Milvus 已随 2026-07-27 精简移除,向量召回未建)

数据流要点(重要)

  • Read / Write 的业务逻辑在 ERP 后端,不在 xlyAi。xlyAi 的工具 = 薄 HTTP 客户端,发和前端一样的 API 请求(erp.baseurl + getBusinessDataByFormcustomId)。
  • Query / NL2SQL 例外:xlyAi 直连 MySQLDynamicExeDbService),不经 ERP 后端 → 这是它击穿权限的根源,需单独锁(见 §7、§9)。

3. Agent 核心

  • 单 agent = LangChain4j AiServices 原生 tool-calling 循环当 ReAct(不用老式文本 Thought/Action;qwen3:14b 支持 function calling)。
  • 退役 8 场景 SceneSelector 路由 —— agent 自路由。 > 实现修正(2026-07-23,见 §17):纯自路由在 qwen3:14b 上不可靠 → 前置一道确定性意图门 + 按意图收窄工具集。 > 这不是回退到场景路由(不再有人设/每表单工具),而是把「判类」从模型的 ReAct 里拿出来单独做。
  • System prompt = 角色 + L1 业务域图(viw_kg_domain, 11 行) + Skill 摘要列表 + 工具用法 + 安全约束。
  • 记忆 = ChatMemory(窗口化 + 超长摘要),持久化到 Redis(键:chat:{userId}:{conversationId},带 TTL)。多条命名会话/用户:每用户一个会话列表,存储按 conversationId 分;按稳定身份(userId+品牌+子公司)寻址,重登录/换 token 后接回历史(token 不是会话 key)。
  • 循环护栏:最大工具迭代步数、单轮墙钟超时、单轮 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 链接。

⚠️ 本节仍是设计,未按此实现(截至 2026-07-23)。实际做法:新 agent 没有immediateReturnToolNames(只有休眠的旧 XlyErpService 用),工具结果仍回喂 LLM;也没有 pendingInteraction 状态机——多轮靠 ChatMemory 自然接续 + 表单提交时前端在消息里塞 proposeWrite(action=create) 标记让控制器识别。卡片/控件由控制器在 onToolExecuted 里推 SSE。 已知代价见 §18(LLM 看得见提议 JSON,可能谎称"已完成",目前只靠 prompt 规则压制)。

前端事件协议(SSE)token(流式文本)/ tool_call(可选可视化)/ question{text,options[]} / write_proposal{diff,stagingRef} / link{url} / error / done

5. 工具规格(6 类通用工具)

通用原则:入参/出参 JSON-schema、无状态、auth 走 per-call context(MCP 形状,便于日后暴露成 MCP)。输出必须分页/截断,绝不吐爆上下文。

下表是能力分类(6 类);实现上落成 9 个 @Tool 方法(Read 拆 readFormData/lookupRecord, KgSearch 拆 findForms/kgSearch,其余各 1 个)。清单见 §17。

# 工具 职责 关键入参 出参 安全 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 池)与用户凭证分离。

token 生命周期 = ERP web 会话(已查实 2026-07-21):xlyAi 转发的 authorization 就是 ERP 登录 token(前端逐请求传入 → XlyErpService/session.setAuthorization → 调 ERP 的 Authorization 头;xlyAi 自带的 RedisTokenManager注释死代码,不自造 token)。长效、非一次性,过期即 ERP 会话过期。⇒ (a) ERP API 返鉴权失败时,xlyAi 向对话框推「登录过期」事件(与网页一致);(b) 会话状态按稳定身份(userId+sBrandsId+sSubsidiaryId)+ conversationId 存,不按 token 存 → 重登录/换 token 后凭同一身份接回历史。autonomy = 永远用户在场触发(已定)→ 无需服务账号/委托令牌,链式续跑只在用户回到会话时发生,凭证模型最简。

⚠️ 授权:后端不是权限权威(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 → 立即回 executing(卡片=处理中,永不 HTTP 死等)→ ERP 检查暂存并在一个事务里执行该行 → 终态经 SSE 推送executed+billId+linkfailed+errorMsg)→ 卡片渲染 ✅已生成[查看]❌失败[重试],结果写会话。链接打开=已建好的单据

  • ⚠️ 原子:execStaging 必须事务化,半途失败自动回滚,重试才安全。
  • ⚠️ 幂等:重试仅当 status=failedsId 作幂等键,防"执行成功但响应丢失→重试→重复建单"。
  • ⚠️ 终态落库 + 重连对账:终态必写 ai_op_queue.status/result(SSE 只是实时推、非唯一真相);前端重连/刷新用 GET /op/{sId}/status(或"我的近期 op")对账,防跳变发生在断线时丢失。

manual 流程(新增/改/删/审核):confirm → ERP 只校验暂存存在且有效(失败/过期→失败卡片)→ 回 link(不执行)→ 卡片渲染 待处理[去ERP操作]xlyAi 到此终止,此卡片不再更新、ERP 不再回响应。 链接打开=字段已预填/预改的原生单据,用户手动点 保存/审核/作废 才真正落库。

  • ⚠️ 代价:xlyAi 拿不到最终单据 id、不知用户是否完成 → 跨 manual 写不能链式(终态交接)。auto 写可链式。将来要 manual 链式需 ERP 完成回调。
  • manual 校验快,/op/confirm同步回链接直接进"待处理"(auto 才恒异步)。
  • 两种链接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_dictKgSearch 工具按需查(L3 必须按词查询,不整表 dump)。
  • 表单卡片 viw_kg_form_card → 可选向量召回(先本地内存或 SQL 匹配,远端 Milvus 后置)。

12. 复用 / 退役 / 新建

内容
复用 AiServices 循环、ChatMemory/OperableChatMemoryProviderVectorizationService、表单 API 客户端、NL2SQL 护栏 + ai_global_agent_question_sql 缓存 + ai_sql_error_history 纠错、jsqlparser、Milvus/embedding
退役 8 场景 SceneSelectorDynamicToolProvider per-form ToolMeta 工具、手写 explainMilvusResult RAG(并入工具)
新建 6 类通用工具、取表单 schema 能力、Skill 注册表 + loadSkill、Redis 会话持久化、L1 prompt 注入、diff 确认端点、ERP 侧暂存执行器 + 预填 + 深链 + 待办入口、审计日志、Langfuse 接入;(设计外补充)意图门 + 按意图收窄工具集 + 确定性槽位填充(见 §17)

13. 迁移路线(strangler,不大爆炸)

  • P0 KG 底座 ✅(sql/ 7 视图,分支 kg-edge-flow)。
  • P1 单 agent 骨架 + Read + Query(只读优先、安全先行) ✅ 代码完成(含表白名单 + 租户注入)。 ❌ 灰度未做:没有选定灰度场景,也没有拿旧 64 工具跑过回归基线对比
  • P2 AskUser(QA) + KgSearch + Skill 注册表 ✅ 代码完成(askUser / kgSearch / ai_skill 3 条种子技能)。
  • P3 ProposeWrite + ERP 暂存执行器 + 预填/深链 —— xlyAi 侧 ✅;ERP 侧执行器/预填/待办端点 ✅ 已写并编译, 但默认关闭erp.exec-staging.enabled=false,本地走直连),生产路径未联调
  • P4 退役旧多智能体/元数据工具;接 Langfuse;报价统一走确认制 —— 退役 ✅(2026-07-27 精简:旧栈/milvus/tts/ocr 全删,见 §18.2);Langfuse ✅(配置开关,默认关);报价确认制待业务。

14. 已定 / 开放问题(见对话讨论)

已定(2026-07-21)

  • 写入需链式 → 必须跨会话状态:写操作不是一次性。skill 可能"下单→据此生成入库",第二步依赖第一步已执行。⇒ 需 ERP 执行后回调通知 agent + 跨会话/长任务状态(会话可在 ERP 执行完后 resume)。这超出单会话 ChatMemory,需要一个持久的「任务/流程实例」存储(Redis/DB)。
  • 审计强制留痕:金融类数据,记不可变审计(谁、何时、什么请求、什么 SQL、暂存/执行了什么)。审计独立于 Langfuse(LLM tracing ≠ 业务审计)。
  • token = ERP web 长效 token(已查实);会话状态按 userId+conversationId 存(非 token),重登录接回;过期向对话框推「登录过期」。
  • 多条命名会话/用户(会话列表侧栏;存储按 conversationId 分)。
  • 前端 = 保留 xlyAi 独立聊天页/xlyAi/chat,演进 templates/chat.html + PageController)。
  • autonomy = 永远用户在场触发(无服务账号/委托令牌;链式续跑在用户回到会话时发生)。
  • 模型 = 指定模型(云/本地皆可),不因出网特殊处理,走配置。
  • 部署 = 共库多品牌xlyweberp_saas,按列区分 sBrandsId/sSubsidiaryId)→ 租户隔离全局强制,KG/缓存/Query 一律按品牌切分。
  • 代码全面改造、写入 master;改造前已完整备份(xlyAi backup/pre-rearch-20260721@ac85337;saas 后端 backup/pre-rearch-20260721@382e97b,快照含工作区,运行中的前后端未动)。

开放

  1. Query 越权锁法:仅 viw_* vs 注入租户谓词 vs 复制 ERP 表单级 ACL —— 待现有 NL2SQL 安全调查结论后定(倾向 A=viw_* + jsqlparser AST 注入租户)。
  2. ERP 侧暂存执行器 + 预填 + 深链 + 执行回调 归谁做、能否排期(写入阶段 P3 跨团队最大单点风险;M1~M2 不依赖它)。
  3. 模型多工具 tool-calling 可靠性 —— 跑起来实测,不行则换模型 / 加参数校验兜底(我自测)。
  4. 多语言:默认仅中文,预留可扩展。
  5. 报价改双确认(失去即时提交)需业务确认(写入阶段再定)。
  6. formId 解析:靠 KG 上下文让 LLM 推理 → 先搭框架、之后补 KgSearch;过渡期表单名模糊匹配 + 不唯一则 AskUser。
  7. 现网安全发现(后端表单权限被关 / NL2SQL 越权 / 读 API 可写)是否作为独立整改上报业务。
  8. 首个灰度只读场景选哪个(建议高频只读轻后果,如库存 / 应收账龄)。

15. 实现状态(2026-07-21,分支 kg-edge-flow)—— 历史快照,勿当现状读

下面记的是当天的状态。工具已在 §16/§17 变过(4 个 propose* 已合并成单个 proposeWrite(action=…))。当前清单见 §17。

已建成、可运行、已验证(xlyAi 全新单 agent 前后端,/xlyAi/api/agent/*):

  • 单 ReAct agentAiServices 流式工具循环(qwen3:14b think(false)),L1 域图进 system prompt,退役 8 场景路由(旧代码休眠未删)。
  • 对话:SSE 流式;多命名会话 + Redis 持久化RedisChatMemoryStore,重启不丢)+ 侧栏。
  • 工具(5)findForms(表单目录/KgSearch雏形) · readFormData(列表/计数) · lookupRecord(单记录全字段) · queryData(只读 SQL 兜底,jsqlparser+LIMIT+审计) · proposeUpdate(HITL 写:改字段)。
  • 写入闭环ai_op_queue 暂存 → 提议不执行 → write_proposal 卡片 → 确定性 /op/{id}/confirm 执行(ERP addUpdateDelBusinessData) / cancel。确认前绝不落地。
  • 审计ai_audit_log 记 propose/confirm/cancel/query。
  • ERP 接入ErpClient dev-login(admin/666666)拿真实会话 token;读 getBusinessDataByFormcustomId,写 addUpdateDelBusinessDatacolumn 列表 + handleType)。

16. 缺口补齐(2026-07-22 上午,分支 kg-edge-flow)—— 历史快照,勿当现状读

上一轮 §15 里「尚未做 / 被外部条件阻塞」的缺口,本轮全部补齐(含跨团队 ERP 侧)。均已编译通过(xlyAi mvn + ERP gradle)。

后续变更见 §17:proposeCreate/Delete/Examine 已合并进 proposeWrite(action=…)collectForm 的字段来源已从 gdsconfigformslave 改为字段字典/精选映射(后者只作兜底)。

xlyAi 侧

  • 授权 + token 透传(§7):新增 AgentIdentity + AgentFactory —— 按每次请求的身份新建 agent,把透传的用户 ERP token 与「表单权限集」(sAuthsIdAuthzService.grantedIds)放进每请求的工具实例(规避 LC4j 流式工具在回调线程执行、ThreadLocal 不可靠的问题,即架构说的 per-call context)。前端逐请求带 authorization+身份;ErpClient 所有读写 转发用户 token用户 token 过期绝不用 dev(admin) 重登(防静默提权)。dev-login 仅作本地兜底。
  • 写:新增 / 删除 / 审核proposeCreate(自动补主键/必填)、proposeDeleteproposeExamine 三个 HITL 工具,均只暂存 ai_op_queue draft、确认端点才执行;确认转发用户 token。ai_op_queuesPayload/sSourceRef/bAutoExecute/sResultBillId/sErrorMsg/tExecutedDate/tExpireAt
  • FormCollect(§5)collectForm 从 ERP 表单元数据(gdsconfigformslave:字段/控件/必填/下拉/默认)取 schema → SSE form_collectxlyAi 聊天页内渲染表单(无需 ERP 前端),提交拼回 proposeCreate
  • AskUser(§5)askUser(question, options) → SSE question → 前端渲染可点选项片。
  • Skills(§6)ai_skill 注册表 + SkillService(name+何时用进 system prompt)+ loadSkill 工具(按需注入完整 playbook);种子技能:新建报价 / 月度对账 / 库存查询。
  • KgSearch(§5/§11)kgSearch 暴露 L2 邻居/流转(viw_kg_form_neighbors)+ L3 字段→表列(viw_kg_field_dict),按词查、截断。
  • 稳健 NL2SQL(§9)QueryTool表白名单viw_* + 表单数据源 + 字段字典表,用 jsqlparser TablesNamesFinder 校验 → 挡住 gdslogininfo/sysjurisdiction/ai_op_queue 等敏感表)+ 单表租户谓词注入sBrandsId)+ prompt 品牌提示 + 自修复重试。
  • Langfuse(§1)TracingChatModelListener 增加配置开关的 ingestion 导出(generation span,走 JDK HttpClient,不引依赖);docker-compose.langfuse.yml 自托管;默认关闭。 > ⚠️ docker-compose.langfuse.yml本机工作区存在但未入库——被全局 gitignore 的 **/docker-compose.*.yml 规则挡掉了, > 换机器/换人 clone 拿不到(见 §18)。

ERP 侧(saas-8s+ release/customer/2025/saas-8s+,backup 分支未动)

  • 暂存执行器 + 预填 + 待办(§10)AiStagingController @/ai —— execStaging/{sId}(POST,执行)、staging/{sId}(GET,预填加载供原生表单深链)、myTodos(GET,AI待办);全部 @Authorization+@CurrentUserAiStagingService(Impl) 读共享库 ai_op_queue → 以用户身份走 ERP 自己的写入/审核逻辑(addUpdateDelBusinessData / doExamine,复用租户隔离、单号、审核存储过程)→ 幂等 + 回写状态/结果。xlyAi 确认端点按 erp.exec-staging.enabled 委托它(生产路径)或直连(本地默认)。

仍属业务/运维决策(非代码缺口)

  • 现网安全发现(后端逐用户表单权限被注释关 / 读 API 可被 bUpdate 触发写 / 旧 NL2SQL 越权)——docs/security-findings.md 已成文,是否整改由业务定。授权层真正“收紧到非管理员”只有在生产以真实用户 token 运行时才能观测(本地 dev-login=admin 全通)。
  • 报价改双确认、首个灰度只读场景选型 —— 业务确认项。

17. 当前实现状态(2026-07-23,分支 kg-edge-flow,HEAD 8b927e3

这是唯一的现状节。改代码后请更新这一节,不要再往 §15/§16 里加。

17.1 工具清单(9 个 @Tool,对应 §5 的 6 类)

类(§5) @Tool 方法 实现类 单例 / 按请求
Read readFormData · lookupRecord ErpReadTool 按请求(带身份)
Query queryData QueryTool 按请求
KgSearch findForms · kgSearch KgQueryTool 单例(全局元数据)
AskUser askUser InteractionTool 单例
FormCollect collectForm FormCollectTool 按请求
ProposeWrite `proposeWrite(action=create\ update\ invalid\
(非工具) loadSkill SkillTool 单例

§16 之后的变化:4 个 propose* 工具合并成单个 proposeWrite(action=…)(贴合 §10「单一写工具」), 并补上 invalid/cancelInvalid(业务单据的"删除"= 作废,可复原)与 cancelExamine(销审)。

17.2 编排:意图门 + 按意图收窄工具集(§3 的实现修正)

AgentChatController.route() 每轮:

  1. 意图门 IntentService.classify() —— 受约束 JSON(OllamaJsonClient),产出 {意图, 单据类型, 带角色实体, missing}
  2. 确定性路由
    • 新增 → 完全不走 ReActresolveMasterFormSlotFillService 按真实字段做正则/角色槽位填充 → 直接弹 collectForm
    • 修改/删除/审核extractWrite{record, field, newValue};齐了就确定性直调 proposeWrite,缺了就问一次即停;
    • 查询 → WRITE 之外的 READ-scope agent(5 个读工具);
    • 不清楚/意图门失败 → FULL-scope 兜底(全部 9 个工具)。
  3. AgentFactory.build(identity, scope)ToolScope 组装工具 + 对应版本 system prompt; maxSequentialToolsInvocations(8) 作循环护栏。

动机(实测):qwen3:14b Q4 在一次 ReAct 里同时判意图/选工具/编参数,会把"查询"当"新增"、 把产品名(纸盒)塞进客户字段、反复追问同一问题。窄任务(意图分类、尺寸拆分)改用受约束 JSON + Java 正则后稳定。

17.3 写入闭环(真实落库,已端到端验证)

  • 报价 = 跨表主-从建单proposeQuote 把 17 个精选字段路由到 quoquotationmaster + quoquotationslave(印刷/部件,sParentId 级联)+ quoquotationmanyqtys(多数量,一档一行), 外键名→id、按列类型强转、自动补主键/租户/制单人/单号(BJD+YYYYMM+序号), 以 __tables__ 结构化 payload 存 ai_op_queue,确认端点走 ErpClient.createMulti 一次提交。 核价(控制/材料/工序行)刻意留给 ERP——那是引擎生成的。
  • 表单渲染带类型collectForm 字段带 fkselect|select|number|date|text;外键下拉由 GET /api/agent/form/options?table=&q= 从来源表实时取(只允许在字段字典里作为外键目标出现过的表,租户过滤 + LIMIT)。
  • 验证记录(真实写入本地 saas 库):BJD202607082(主表)、BJD202607084(主+从+多数量)、BJD202607086(含多数量 2000/4000/6000)。

17.4 与设计的已知偏离

设计 实际 影响
§4 immediate-return 工具 未用(新 agent 没设 immediateReturnToolNames 提议 JSON 会回喂 LLM,可能谎称"已完成",目前只靠 prompt 规则压制
§4 pendingInteraction 状态机 未建,靠 ChatMemory + 表单提交时消息里的 proposeWrite(action=create) 标记 多轮 HITL 无显式状态,跨轮续跑脆弱
§10 卡片三态 + auto 异步 + SSE 终态推送 恒同步:确认即执行并同步返回终态 长事务会 HTTP 死等;无重连对账
§10 manual 预填深链 /form/{formId}?staging={sId} ERP 侧端点已写,xlyAi 侧不产生这类链接 manual 流程未打通
§3 agent 完全自路由 前置确定性意图门 见 §17.2(有意为之)

18. 未完成清单(2026-07-23)

阻塞上线

  1. kg-edge-flow 未合并 master —— §14 已定"写入 master",但全部 6200 行新代码只在该分支;master 仍是改造前状态。
  2. 旧栈未退役且仍在跑已退役(2026-07-27 精简):删除 XlyErpService/8 场景路由/DynamicToolProvider/Scene·ToolMeta 缓存、整个 milvus/tts/ocr 包、/api/tts/api/ocr 端点、tts.html 及 chat.html 死 JS;pom 依赖同步裁剪(milvus/embeddings/mybatis/ocr/webflux 等)。代码只剩新栈 ~40 个类。
  3. 零自动化测试:无 src/test,每次都是手工跑对话验证;重构无回归网。
  4. 生产路径未联调erp.exec-staging.enabled=false;ERP 侧 AiStagingController 只编译过、没被真实调用过。
  5. 授权层未在非管理员下验证:本地 dev-login=admin 全通,AuthzService.grantedIds 的收紧效果无法观测。

功能缺口

  1. pendingInteraction 状态机 + immediate-return(§17.4)。
  2. 异步执行 / 卡片三态 / SSE 终态推送 / GET /op/{sId}/status 重连对账(§10)。
  3. manual 预填深链与「我的 AI 待办」在 xlyAi 侧的入口。
  4. ai_op_queuetExpireAt 过期清理无调度任务;bAutoExecute 建了但没有分流逻辑。
  5. 报价核价结果不回读——建完单不知道算出多少钱,用户得自己去 ERP 看。
  6. Skill 只有 3 条种子且未在真实对话里验证过;loadSkill 无使用数据。
  7. KgSearch 的向量召回未接(只有 SQL 关键词匹配)。2026-07-27 精简已把 milvus/embedding 代码与依赖整体删除——若做 5b 需按新栈重新接(milvus-sdk + embedding 依赖重加,旧实现在 git 历史 8b927e3 前可查)。
  8. 报价之外的单据没有精选字段映射businessFields 走字段字典启发式,复杂单据大概率不准。

安全 / 运维

  1. Query 的租户 AST 注入只覆盖单表,多表 JOIN 仅靠 prompt 提示;只读 MySQL 账号未启用(与应用共用连接池)。
  2. docker-compose.langfuse.yml 未入库(被全局 gitignore **/docker-compose.*.yml 挡掉);Langfuse 默认关闭、从未真正导出过 trace。
  3. 现网安全发现待业务决策(见 docs/security-findings.md)。

业务决策项

  1. 首个灰度只读场景选型;报价是否改双确认;多语言(当前仅中文)。