ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

把 AI Agent 接进真实业务:意图识别与业务系统的解耦

把 AI Agent 接进真实业务:意图识别与业务系统的解耦 摘要上一期我们把 Agent 的壳造出来了P-D-A 模板方法、双 Memory、MCP 化的工具层、Router 多 Agent 调度。但壳只是骨架接进真实业务是另一回事——用户说的话千奇百怪业务系统有自己的登录态、租户、数据边界。这一期讲两件事意图识别怎么做才能稳以及Agent 壳和业务系统怎么解耦。我的核心观察是意图其实是分层的——路由意图分发到哪个 Agent、业务意图查哪类数据、工具选择调什么工具怎么调每一层的实现模式完全相同确定性规则优先、LLM 居中、规则降级兜底。而解耦的答案是中间加一层薄的适配层Agent 只认name params result业务只认 Service 方法两端都不认识对方的技术栈。做完的体会是LLM 接得再好也只是漏斗中间的一环真正决定能否上线的是漏斗两头的规则和降级。特别说明本文是阶段性方案记录。跑通多 Agent 协作是真达到生产级效果是未完成的目标——文末现状与局限一节会原样交代不吹不藏。一、上篇接上壳造好了剩下的两个问题上一期的架构回顾下组件上期解决的问题P-D-A 模板方法Agent 的执行骨架感知 → 决策 → 行动双 MemoryWorking Memory 管步骤间数据、Conversation Memory 管跨轮记忆MCP 工具层工具定义一处出、三处用MCP Server、LLM Prompt、内部执行Router 专业 Agent多 Agent 协作调度但能用和能接进真实业务之间还隔着两个问题意图识别不够稳。上一期说了三道坎JSON、幻觉、降级但没有系统回答意图判到什么粒度判断错了怎么办多个意图混在一句话里怎么办他不知道我问什么怎么办壳和业务是拧在一起的。Agent 直接注入业务 Service 的话加一个业务能力就要改 Agent加一个 Agent 就可能动业务代码两边一起腐化。这一篇意图部分我给出一张完整的三级漏斗设计解耦部分给出三刀切的边界划分。二、意图识别一条三级漏斗先给结论。这套系统里意图不是一个概念是三个层级层级谁负责回答的问题输出第 0 级路由意图RouterAgent这句话该哪个 Agent 管targetAgent第 1 级业务意图BusinessQueryAgent 感知层用户想查哪类业务intent entities第 2 级工具选择BusinessQueryAgent 决策层调哪个工具、什么参数toolName action params三层各有一个特征LLM 只负责中间那个单点判断前后全被确定性规则包住。每层的模式一模一样所以每一层我都能单独加防御、单独降级不存在LLM 一挂全挂的路径。2.1 第 0 级路由意图Router 的三层闸门Router 收到你好、查一下今天的订单这种输入走三条闸门第一层关键词预路由 命中「查询/订单/统计/审核/活动/营收/报表」→ businessQuery置信度 0.95 第二层高频聊天兜底 命中「你好/谢谢/在吗/你是谁/hello」→ chat置信度 0.95绕过 LLM 第三层LLM 感知 都没有命中 → 交给 LLM 输出 targetAgent confidence reason前两层是纯代码一行 LLM 都不调——明显是业务查询的别给模型误判的机会明摆着是打招呼的别花一次 API 调用的钱。这个顺序是踩过坑才定的早期把所有输入都丢给 LLM结果你好经常被幻觉成业务意图连在吗这种词都能被编出一个假查询来。规则先拦截LLM 只处理真正模糊的输入。第三层 LLM 感知返回后还有一道闸白名单校验。LLM 输出的 targetAgent 必须是注册表里真实存在的否则直接降级到 chatif(agentRegistry.getAgent(targetAgent)null){log.warn([RouterAgent] LLM 返回的 Agent {} 未注册降级为 chat,targetAgent);targetAgentchat;}2.2 第 1 级业务意图感知层的五步流水线输入进了 businessQuery第一件事不是找 LLM而是先净水。BusinessQueryAgent 的感知层是这么走的protectedPerceptionperceive(AgentRequestrequest){// ① 指代消解用对话记忆替换代词LLM 调用前执行提高理解准确率StringresolvedInputresolvePronouns(input,request);// ② 关键词预判断LLM 对某些输入会 hallucinate 乱码先做确定性判断StringkeywordIntentmatchIntentByKeyword(normalized);if(keywordIntent!null){...置信度0.9直接返回}// ③ LLM 语义理解首选// ④ 白名单校验intent 必须属于预定义枚举非法值降级关键词匹配// ⑤ Fallback关键词匹配}这五步里有两个容易被忽略的细节关键词预判断在 LLM 之前不只作为降级。代码注释原话是“LLM 对某些输入会 hallucinate ‘乱码’先做确定性判断”。短输入、纯关键词输入LLM 经常给你胡编乱造而这类输入恰恰是规则最容易判对的。确定性的先跑不是逼不得已才用规则而是规则能做的别麻烦 LLM。业务意图有白名单且白名单就是 Prompt 里规定的枚举。感知时刻度从order / user / activity / dashboard / content / review / unknown七个值里选两边严格一致LLM 只要敢多嘴输出描述性文字比如订单查询直接降级SetStringvalidIntentsnewHashSet(Arrays.asList(order,user,activity,dashboard,content,review,unknown));if(!validIntents.contains(intent)){log.warn([BusinessQueryAgent] LLM 返回非法意图值: {}降级为关键词匹配,intent);returnperceiveWithKeywords(originalInput);}置信度也是成套的方便日志里一眼看出判断来源判断来源置信度关键词预路由感知前置0.9关键词匹配单意图0.85关键词匹配多意图 multi.query0.75LLM 语义理解LLM 自报非法值回填 0.85unknown0.02.3 实体抽取从话里挖出查询条件意图只是要什么实体才是条件。这套系统的实体抽取分四类前三类是规则的一类是 LLM 的同义词组。一张 12 组的映射表把口语归并成规范意图。比如订单/报名/缴费/支付/交易/购买记录 → order营收/收入 → dashboard.order。匹配时按关键词长度从长到短排序还要打标记防重叠——“订单统计里命中订单又命中统计”两个意图都得报但订单数据这种嵌套词不能重复记位置// 匹配过的字符位置做标记防止同一段文字被多个同义词重复命中SetIntegermatchedPositionsnewHashSet();if(idx0!overlaps(idx,idxkeyword.length(),matchedPositions)){...}时间词。今天/今日→today, 昨天→yesterday, 本周→week, 本月→month, 上月→lastMonth, 近7天/近30天 → 7days/30days一张TIME_KEYWORDS表直接翻译成内部枚举不进 LLM。数量词。一条正则把前 3 个最新的 5 条挖成分页大小Pattern.compile((?:前|最新的|最近)[\\s]*([0-9])[\\s]*(?:个|条|项));指代消解在 LLM 之前做。上期讲的 Conversation Memory 在这里派上关键用场第二轮用户说他的订单呢感知层先从记忆里取出上轮实体keyword张三把他/她/它替换成张三再喂给 LLM 或关键词。先修输入再谈理解——让 LLM 在已经说人话的输入上工作比把指代问题丢给它猜要稳得多。2.4 第 2 级工具选择决策层永远有规则兜底感知确定了业务意图决策层负责怎么查。同样是 LLM 优先、规则兜底LLM 决策根据意图 实体输出执行计划 JSONtoolName action params解析失败 / 非法 → 规则映射一张intent → toolName action 参数的硬编码表12 个意图全都能精确翻译意图为 unknown →直接跳过 LLM 决策走规则这里有个有意思的场景一句话里混了多个意图。关键词匹配命中多个时感知层输出multi.query置信度 0.75然后把命中的意图列表塞进实体。决策层拿到后先查链式模式表privatestaticfinalString[][]CHAIN_PATTERNS{{user,order},// 先查张三 → 再查张三的订单{user,dashboard.order},// 先查张三 → 再查张三的订单统计{user,dashboard.review},};命中链式模式的按链路的先后拆成两个步骤执行第二步通过 Working Memory 的_previousResult吃第一步的结果上期讲过机制这里不再展开没命中链式模式的各意图平铺成并列的多步骤。查订单顺便看统计这种复合句用规则拆解比让 LLM 自己编排稳得多。三、unknown的哲学不要让 LLM 自由发挥整套意图识别里我花心思最多的是 unknown——识别不出意图的情况。它的处理逻辑顺着三关走感知层意图 unknown置信度 0 决策层unknown 不走 LLM 决策防止 LLM 拿到 unknown 后自由发挥、瞎编计划 行动层steps 为空 → 不执行任何工具 → 用 chat-fallback Prompt 让 LLM 生成友好引导三段实现分别是// 决策层unknown 直接走 fallback 规则if(unknown.equals(perception.getIntentType())){log.warn([BusinessQueryAgent] 意图为 unknown直接走 fallback 规则);returndecideWithRules(perception);}// 行动层查不了也要体面 —— 用 LLM 生成友好回复而不是硬编码报错if(decision.getSteps()null||decision.getSteps().isEmpty()){returnactWithLLM(decision);// chat-fallback Prompt介绍能力 给出查询示例}为什么 unknown 不能走 LLM 决策因为它是个信任陷阱未知意图恰恰是 LLM 最自由、最不可控的时候。感知层给不出置信度决策层再放权等于把两道闸门同时打开。unknown 的正确归宿不是猜而是诚实地说不知道并告诉用户能干什么。这也是上期铁律二的延伸——确定性逻辑不进模型模型只在单点做判断。顺带一提chat-fallback 的 Prompt 也是外置在agent-prompts.properties里的LLM 不可用时还有一段硬编码兜底文案保证查不了时至少有个像样的回答。四、解耦第一刀Tool 适配层Agent 不认识业务上面对意图的识别都在 Agent 层完成但一旦要落地查数据就撞上了真实业务。第一刀就是把AI 侧和业务侧用一层 Tool 隔开┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ Agent 框架层 │ │ Tool 适配层 │ │ 业务 Service 层 │ │ Agent/Engine/ │ → │ AgentTool 契约 │ → │ UsersService │ │ Registry/LLM │ │ UserTool/Order… │ │ OrderService… │ └─────────────────┘ └──────────────────┘ └─────────────────┘ 不认识 MyBatis 只认 nameparamsresult 不认识 LLM/PromptTool 的契约极薄就四个方法publicinterfaceAgentTool{Stringname();// 工具名如 userToolStringdescription();// 给 LLM 看的描述StringgetParamSchema();// 参数格式说明ToolResultexecute(MapString,Objectparams);// 执行}每个 Tool 本质上就是业务 Service 的薄适配器Tool注入的业务 Service暴露的能力UserToolUsersService用户列表、按关键词/手机号搜索OrderToolBusinessOrderService订单列表、状态统计、近 7 天趋势DashboardToolDashboardService用户/订单/课程/社区/告警/审核统计、订单趋势ActivityTool / ContentTool / ReviewTool对应业务 Service活动管理、内容管理、审核流好处是什么两侧都变简单了Agent 层没见过任何业务代码。感知、决策、Prompt、LLM 调用全在 Agent 里它唯一知道的业务口子是有个叫 orderTool 的东西传 params 能出 result。加一张表、改一个查询Agent 一行不用动。业务层没见过任何 AI 代码。Service 该查库查库、该分页分页完全不知道自己被 Agent 调用了。哪天把 Agent 框架整个换掉业务层零改动。一切皆 Map这是刻意的取舍LLM 天然产出 JSON工具天然消费 Map中间少一层对象转换。代价是类型不安全——所以每个 Tool 内部都有自带的类型防线// LLM 把 123 当字符串返回是常态安全解析 默认值兜底privateintgetInt(MapString,Objectmap,Stringkey,intdef){Objectvmap.get(key);if(vinstanceofNumber)return((Number)v).intValue();if(vinstanceofString){try{returnInteger.parseInt((String)v);}catch(Exceptione){returndef;}}returndef;}这个防线不是防御编程洁癖LLM 是唯一会把参数类型说错的调用方。它不是坏是不在意——字符串123和数字123对它都是一百二十三。类型转换写在离调用边界最近的地方Tool 入口一次挡掉所有下游。五、解耦第二刀上下文注入与多租户Tool 解耦了能力但真实业务还有个绕不开的身份和数据边界。同一个 Agent 实例要为所有登录用户服务每个人的数据必须只看到自己的。这一刀落在上下文注入。系统里有个贯穿链路的数据契约——ServiceContext携带 userId、userName、roleIds、tenantId租户等登录态信息。注入链路是AgentEngine.execute() → ContextHelper.getServiceContext() // 主线程 ThreadLocal 取登录态 → agent.setServiceContext(sc) // 注入 Agent → Tool业务查询时透传 → 业务 Service按租户/用户过滤数据两个真实的坑都跟这条链路有关坑一SSE 线程上下文丢失。流式执行跑在新线程里Spring Security 的 ThreadLocal 不会跟着走——这期代码里明确地在 Controller 主线程捕获 ServiceContext、注入执行线程、finally 清理。接进真实业务的前置条件不是 Prompt 写得有多好而是请求的我是谁在任何线程里都丢不了。坑二Agent 是单例上下文必须每请求注入。Agent 实例是 Spring 单例绝不能在 Agent 里存上一个请求的会话状态否则就是用户 A 的数据串到用户 B 头上上一期的 Conversation Memory 按 userId 隔离就是为了这一点。框架保持无状态上下文每请求注入数据边界由 ContextHelper 这条链路保证。多租户同理同一个 Agent 服务所有租户是 tenantId 决定了它能看到哪片数据。六、解耦第三刀注册中心自动装配最后一刀落在加东西这件事上。AgentRegistry 在 Spring 启动时自动扫描所有Agent和AgentTool的 Bean 注册新类只需一个ComponentPostConstructpublicvoidinit(){agentBeans.forEach(this::registerAgent);// 自动注册无需任何配置toolBeans.forEach(this::registerTool);}于是扩展路径就固定下来了全程无跨界修改要做什么要动的文件不动的东西加一个业务 Agent1 个新类 properties 加 Prompt Router 登记框架、引擎、注册中心加一个业务能力1 个新 Tool McpToolRegistry 加一段定义Agent、Prompt、MCP Server加一个查询 actionTool 内部加 caseAgent 感知决策、LLM Prompt特别是第二行工具定义在McpToolRegistry.buildDefinitions()里是唯一数据源——加一段定义LLM 决策 Prompt 的工具描述和 MCP 的工具列表同时自动同步。上期讲过这个机制这期从解耦角度再看一遍它的价值它保证AI 侧看到的工具清单和业务侧真实存在的能力永远一致不会出现 LLM 想调一个不存在的工具、或者真实能力没暴露给 LLM 的两头断档。衡量解耦是否到位的标准就一句话加业务能力时Agent 框架的代码一行都不应该被修改加 Agent 时业务代码一行都不应该被修改。如果出现加个字段还要动 Prompt 模板“加个报表还要改 Router”说明边界画错了。七、现状与局限跑通了协作还没跑通生产说句真话。这套系统在多 Agent 协作与接驳真实业务这个目标上是跑通的三级漏斗能走、三刀解耦能加东西、降级链路全程实测过。但离生产级效果还有明确的距离这也是我把现状原样写出来的原因——工程文章最不该骗人的地方就是告诉大家它真的能跑了。实测暴露的问题有三个1. 训练不够。当前意图识别完全靠 Prompt 引导模型理解业务没有针对业务语料的微调。LLM 对口语化表达、行业说法、绕来绕去的长句理解鲁棒性明显不足——同一个问题换个说法意图就可能飘。2. 模型选型不友好。当前模型对中文业务指令的理解和遵循都不够友好幻觉率偏高而且它经常在非常自信的状态下给错——自信的错比不确定的错更难防因为置信度字段拦不住它。3. 没有评测集。这是最该补的功课。现在只能靠日志人工抽查来评估精度准确率多少、降级触发率多少、哪种错误最多这些数字全部缺席。没有度量就没有改进的依据。下一步按这个路线走步骤动作产出建评测集收集真实对话含错误案例每条标注期望意图一套可反复回归的评测集模型策略对比在同一个评测集上对比微调 vs 换模型用数据决策而不是拍脑袋安全网不动规则预路由 降级链路原样保留无论模型怎么换都兜得住收个尾这套代码的价值是一个可运行的骨架 一条诚实的基准线而不是一个能上线的 AI 助手。它证明了多 Agent 协作在 Java 生态里能落地、边界在哪也坦诚地标出了距离生产还差多少。后者对做技术的读者可能比前者更有用——知道别人做到哪、卡在哪比自己重新踩一遍省时间。八、三条体会上篇立了三条铁律这篇的实践再补三条意图是分层的每层都是规则优先、LLM 居中、降级兜底。路由意图、业务意图、工具选择三层层层的防御结构一模一样。永远不要在某一层裸奔——你永远不知道 LLM 下一次会在哪一层犯浑。给 LLM 的每一句话都是在给它下需求。感知 Prompt 里规定意图枚举决策 Prompt 里拼工具描述连unknown 怎么办都写成固定流程——整个意图识别不是调一个模型是把人的判断规则翻译成 Prompt再把模型的输出翻译回规则。解耦的收益要在加东西的时候兑现。一个架构好不好开发时就看得见加一个新 Agent、一个新业务能力是局部新增还是跨界改动就是架构的分水岭。下一期预告既然距离生产差在效果不可度量下一篇就围绕它写——《给 LLM 立规矩评测集、模型对比与降级策略》把怎么判断一个模型到底能不能上线的方法讲透。文中所有实现均来自我在实际项目里的完整落地代码与架构图可在评论区留言交流。本文为上篇《搭建属于自己的 AI 智能体以及多 Agent 协作》的续篇。
返回列表