xlyAi Agent 目标架构
状态:设计已收敛(2026-07-21)。全 Java / LangChain4j,单 ReAct agent + 通用工具 + 人在环写入。 关联:知识图谱底座见
sql/README.md;现状代码接线见 memoryxlyai-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时真改库存表。
⇒ 新增架构组件:xlyAi 侧授权层。 用用户 sAuthsId/授权菜单把 agent 限制在用户实际有权的表单/动作集内(Read/Query/Invoke 通用),等于把 UI 菜单权限搬到 agent 侧。方案 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 可碰的表单/动作;现网发现"后端权限被故意关"待报业务。 -
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 对话内【最终确认】 → 落暂存 sp_ai_addcommonwx
- 报价即使已用 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现在没有任何消费者(Sp_Ai_AddCommonAfterNew只入队从不执行)→ ④ 执行器是必须新建的 ERP 侧组件,最重、跨团队。 -
统一:报价也走此路(废
Sp_Ai_AddQuoQuoAfter直连),全链一致。 -
暂存记录(在现有列基础上扩展):
{sId, createdBy(用户), targetFormId, targetModuleId, op(new/copyto), payload(JSON), sourceRef, status(draft→staged→confirmed→executed/failed), link, expireAt, tCreate}。 - 需处理:幂等(重复确认)、过期、确认前底层数据变化、状态机、失败回执。
-
前端:ERP 需一个「我的 AI 待办」入口 + 深链路由(
stagingRef→ 打开对应表单并预填)。
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 ≠ 业务审计)。
开放
-
Query 越权锁法:仅
viw_*vs 注入租户谓词 vs 复制 ERP 的表单/字段级 ACL —— 待现有 NL2SQL 安全调查结论后再定(正在后台查:现状是否有租户过滤、是否会越级)。 - 部署形态:单品牌一实例 vs 多品牌共库(决定 KG/缓存/Query 是否按
sBrandsId切分)。 - ERP 侧暂存执行器 + 预填 + 深链 + 执行回调归谁做、能否排期。
- 模型:qwen2.5:14b 多工具 tool-calling 可靠性(否则换模型 / 加 tool-call 参数校验兜底)。
- 多语言:仅中文 vs 支持 en/big5。
- 报价改双确认(失去即时提交)需业务确认。
- HITL
pendingInteraction+ 跨会话任务状态语义 + 前端事件协议,与前端团队对齐。 - KG 卡片召回:本地内存向量 vs 远端 Milvus 时机。
- 首个灰度只读场景选哪个。