多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

从一句话到可靠交付:AI Agent完整工程链路拆解

从一句话到可靠交付:AI Agent完整工程链路拆解 带智能体项目跑了半年多我最大的感受是用户不会按你设计好的交互路径说话他们只会甩过来一句话。这句话可能模糊、有歧义、缺上下文甚至夹杂着上一条对话里的信息。而我们真正要交付的不是理解这句话而是把这句话背后的事情办成、办好、可追溯。从一句话到可靠交付中间隔着一整套工程链路意图识别、多Agent编排、任务规划、质量评审、异常恢复、以及一个经常被忽略但必须从第一天就做好的——幂等。这篇文章想把这条链路完整梳理一遍。不是教科书式的架构讲解而是基于我实际搭建伴AI这套系统时踩过的坑、改过的方案、验证过的逻辑。适合已经在做Agent应用、或者准备从Demo走向生产的团队参考。1. 意图识别不是分类问题是上下文协商问题很多团队第一次做意图识别最自然的想法是训练一个分类模型把用户的话归到几个预定义意图里。这个思路没错但它只适用于一句话对应一个动作的简单场景。真实生产环境里用户的一句话往往包含着多个意图、隐式条件、以及对话历史里才有的指代信息。1.1 意图识别与参数抽取必须同步做我踩过的第一个坑是把意图识别和槽位填充拆成了两个独立阶段。先判断意图再抽参数看起来逻辑清晰实际运行中却频繁出问题。原因在于意图和参数是互相约束的——用户在说帮我把上个月的周报改成PDF发给我时改格式和发送是意图上个月周报PDF是参数但如果先识别意图模型在修改文档和发送文件之间摇摆就会严重影响后续参数抽取的位置。后来我把整个识别流程改成了同一模型输出结构化结果一次推理同时给出意图列表、参数列表、以及每个参数在原文中的引用片段。结构大致是{ intents: [ {name: format_convert, target: monthly_report, target_format: pdf}, {name: send_file, channel: email, receivers: [team]} ], coreference: {上个月: 2025-02-01~2025-02-28} }这样做的好处不仅是减少一次推理延迟更重要的是让系统能处理一句话里有多个动作的情况。拆成两个阶段时第二阶段根本不知道该以哪个意图为准去抽参数合并输出后模型可以一次性看到全局信息。1.2 置信度阈值与意图兜底策略落地时还有一个绕不开的问题模型的置信度分到底怎么用。把阈值设得过高用户的话稍微绕一点就会被拒设得太低误判会直接触发错误的执行分支造成比拒答更严重的后果。我的做法是设置三道防线高置信度0.9以上直接进入任务规划阶段不再追问。中置信度0.7到0.9进入澄清模式把识别出的结果用自然语言回显给用户让用户确认而不是干巴巴地列一个问题列表。低置信度0.7以下不硬猜切换为通用问答或人工兜底绝不擅自触发执行动作。关键心得是意图识别的失败率不可怕可怕的是失败之后还继续走流程。宁可多一次澄清交互也不要让用户看到我明明说要改PDF你发了个Word这种低级错误。1.3 冷启动没有标注数据怎么办提到意图识别团队第一反应往往是先标注几千条数据训练一个BERT或者直接用大模型微调。这当然可以但项目前期最缺的就是数据。我的建议是先用大模型Few-shot加规则兜底跑起来产品形态验证无误后再积累数据做专项优化。具体做法是在系统里加一个识别结果日志表把每个请求的原始文本、识别结果、用户后续是否纠正、最终是否被采纳都记录下来。跑两周后用这些日志筛选出识别正确但用户仍不满意和识别错误但系统没发现的样本做定向分析和补充标注。这样做比凭空拍脑袋造训练数据有效得多。2. 多Agent编排的核心角色分清楚协作定规则多Agent这个词这两年被炒得很热但很多项目只是把几个Prompt拼在一起就对外叫多Agent系统。我的理解是多Agent不是堆模型调用而是把不同的职责边界、不同的上下文视角、不同的工具权限拆分到独立执行单元里再通过明确的协作协议组合起来。2.1 Agent的角色划分到底按什么标准我设计的伴AI系统里有四类Agent划分标准不是模型能力不同而是职责边界和上下文隔离需求解析AgentParser负责意图识别、参数抽取、指代消解。只阅读输入不输出执行动作。规划AgentPlanner负责把意图转化为具体的任务序列和依赖关系。阅读任务上下文不接触外部系统。执行AgentExecutor按规划结果调用具体的工具、API、代码。可以访问外部系统但没有修改全局任务状态的权限。评审AgentReviewer对执行产物做质量检查独立于执行链路运行不带执行阶段的预设立场。为什么要把它们拆开因为我发现如果把执行和评审放在同一个Agent里模型很容易自己评价自己时放水。独立评审Agent的Prompt里甚至明确写了你不参与任务的执行过程你的唯一职责是找出缺陷效果才会不一样。2.2 协作协议比Agent本身更重要Agent之间如何协作是需要明确设计的关键点。我实际用的是两种模式的组合流水线模式适合任务边界清晰、顺序固定、无需多轮交互的场景。例如解析 - 规划 - 执行 - 评审。会商模式适合执行分支模糊、需要多方案评估的场景。例如遇到这个需求可以做A方案也可以做B方案时由规划Agent发起讨论各Agent发表观点最后由规划Agent汇总并推进。会商模式最大的风险是对话轮次失控模型之间互相扯皮产生大量无意义中间内容。我的解决办法是设置最大讨论轮数上限一般三轮超时后由规划Agent直接基于已有信息做决策并强制收敛。宁可在决策质量上损失一点也绝不能卡死在无限循环里。2.3 Agent上下文传递的边界设计多Agent系统里最容易出问题的不是Agent能力而是上下文混乱。A Agent的中间结果被无关的B Agent读取主对话的历史消息被带进工具调用的上下文上一轮的执行结果污染了这一轮的推理判断——这些都是常见问题。我最后定的上下文传递规则很简单每个Agent只能拿到它完成当前任务所必需的最小上下文集合不能默认看到全部对话历史。具体在代码里就是每次调用Agent时显式传入允许访问的字段白名单而不是把整个状态对象丢给模型。这样能减少token消耗更重要的是避免幻觉和误读。一次实际运行中规划Agent因为看到了执行Agent的内部调试日志居然把日志里出现的示例文件名当成了真实交付物这让我彻底下决心做了上下文隔离。3. 任务规划的粒度决定了交付的上限意图识别完之后系统知道了用户要什么但离怎么交付还有很长一段距离。规划Agent要做的是把意图拆解成一条可执行的任务链路。规划质量直接决定后续执行和评审的复杂度——规划得糙执行阶段就要频繁返工。3.1 拆到多细才算一个原子任务我定义原子任务的标准很朴素一个任务单元可以独立执行、独立验证、独立失败重试且不需要在执行半途再向用户追问信息。凡是达不到这个标准的说明拆得还不够细凡是拆出来只有一步却能写出很多废话的说明拆得过碎了。举例来说用户说整理近三个月项目周报汇总成一份PDF发团队。合理拆解应该是获取近三个月周报的原始数据调用数据源API按模板汇总生成Markdown草稿将Markdown渲染为PDF校验PDF的页数和内容完整性获取团队成员邮箱列表发送邮件并保留发送凭证第4步校验PDF完整性是我特别加强的。没有这一步前序任何环节出错比如模板字段缺失导致PDF生成异常都会直接流入交付环节直到用户打开PDF才发现问题。这就是评审前置的思想——不能到最后才检查要在每个关键产出物生成后都做轻量级验证。3.2 依赖关系与并行策略的计算任务之间不是简单的顺序列表。有的任务可以并行有的强依赖前序结果。我采用DAG结构来描述任务依赖每个节点记录自身的输入来源和输出去向。并行策略上我踩过一个问题贪心地并行所有无依赖任务看起来速度快了但实际资源开销大还有副作用——多个并行Agent同时修改共享状态时容易产生冲突。后来我改成有控制的并行只并行那些耗时占比高且无状态的独立任务比如多个数据抓取任务有状态写入的任务一律串行。3.3 规划结果必须先确认再执行这一点是我最想强调的。尤其在涉及外部副作用发邮件、改文件、调用生产API的任务上规划结果必须经过用户的确认才能执行。系统把任务拆解结构、预计执行步骤、涉及的外部操作、预期产物展示给用户用户确认后才继续。有团队担心这样是不是太繁琐了用户体验不流畅。我的实际经验是受控的确认对于建立用户信任的长期价值远大于在单次交互里节省的那几秒时间。用户确认过程本身也是一种隐性的质量校验——如果用户在确认阶段就发现系统理解偏了那等于用最低成本避免了后面全链路返工。4. 质量评审把好交付的最后一关质量评审模块是从Demo走向生产时最容易偷懒的地方。很多团队觉得反正大模型生成的内容用户自己会看有问题再说。但作为交付系统用户要的是拿到手就能用而不是拿到手还要我来校对。4.1 评审的检查维度怎么定我设计的评审维度不是笼统的质量高不高而是拆成可量化的子项维度检查内容判定方式完整性是否覆盖用户需求的所有必交要素需求清单比对格式合规是否符合目标格式的规范PDF页数、字段结构规则校验信息一致性正文中的数据、人名、日期是否与源数据一致交叉验证语义连贯性生成文本是否通顺、逻辑无矛盾LLM评审安全合规是否包含敏感信息或不当内容规则引擎前四项大家都容易想到第五项安全合规是后来加的。有一次系统生成的交付物里意外包含了源文档中一段无权限公开的内部备注而评审Agent完全没注意到。之后我加了一个独立的敏感信息扫描规则引擎用正则和实体识别去扫交付物而不是依赖LLM自己发现。4.2 LLM评审和规则引擎的双轨制说实话纯LLM评审不稳定。让它找逻辑问题、语义偏差它很擅长让它验证文件够不够大数量对不对格式严不严格它经常出错。反过来规则引擎擅长精确校验但不理解语义。所以我把评审设计成双轨并行硬性指标走规则引擎软性质量走LLM评审。规则引擎负责文件格式、字段完整性、数据一致性、敏感信息扫描LLM评审负责内容是否有歧义、是否偏离用户原始意图、逻辑是否自洽。两轨结果合并为最终评审报告任何一轨不通过任务都回到执行阶段修改。双轨制还有一个好处可解释性。规则引擎可以明确输出哪一项检查不通过、期望值是多少、实际值是多少LLM评审则可以给出自然语言的修改建议。这两者组合起来后续的自动修改引擎才会有操作依据。4.3 评审不通过后的修改循环评审发现问题后不是简单地把任务从头跑一遍而是要把评审意见作为定向修改指令交给执行Agent。修改阶段的Agent拿到的上下文包括原始需求、原始产物、评审报告包含具体问题和建议修改方向然后针对性地产出修订版本。这里要设置迭代上限实践中我设的是三轮。三轮仍未通过评审则停止自动修改进入人工处理通道。原因是避免模型在循环里反复自我折磨既消耗资源又大概率收敛不了。实际运行中80%的问题在前两轮就能解决剩余的多是需求和产物本就不匹配确实需要人来判断。5. LangGraph恢复机制流程崩溃不再从头来过系统进入生产后一定会遇到各种中断网络超时、API返回异常、模型上下文超限、服务重启。如果在这些场景下整个任务回到起点重新跑不仅浪费资源更糟糕的是用户等待时间无法接受。所以流程编排层必须支持断点恢复——LangGraph在这里扮演了关键角色。5.1 为什么需要检查点机制而不是简单重试第一版系统里我对整个任务做的是粗粒度重试失败了就从头开始认为只要幂等设计做得好重跑也无所谓。但实际上很多任务链的中间产物比如已完成的数据清洗结果、已生成的PDF初稿可以复用从头跑意味着这些有效工作全部作废。LangGraph的检查点机制解决的核心问题是在每个节点执行完毕后持久化当前状态包括节点输入输出、执行环境快照、以及相关元数据。这样在失败恢复时系统可以精确地定位到失败的节点并只重放该节点及其后续节点而不是重放全部链路。5.2 断点续跑的具体实现方式以伴AI的LangGraph集成而言我在地图使用checkpointers持久化状态。每当super-step完成时整个图的状态就提交一次。这意味着节点内部如果卡住了比如调外部API超时不会产生任何状态变化只有整个节点成功执行完毕状态才会推进到下一个节点。# LangGraph检查点机制的核心用法 graph StateGraph(WorkflowState) graph.add_node(parse_node, parse_node) graph.add_node(plan_node, plan_node) graph.add_node(execute_node, execute_node) graph.add_node(review_node, review_node) graph.set_entry_point(parse_node) graph.add_edge(parse_node, plan_node) graph.add_edge(plan_node, execute_node) graph.add_edge(execute_node, review_node) checkpointer MemorySaver() app graph.compile(checkpointercheckpointer) # 执行任务传入thread_id用于恢复 config {configurable: {thread_id: task-12345}} result app.invoke(initial_state, config)这里的关键是thread_id。它就是任务实例的唯一标识恢复时只要用相同的thread_id调用LangGraph就能从最近一次检查点继续执行而不是重新从入口开始。我第一版犯过一个错误每个节点执行前重新构造状态导致恢复时部分中间结果丢失后来改成在节点定义中严格遵循读输入状态、执行动作、写输出状态的模式状态流转才变得可控。5.3 恢复之后的上下文一致性检查点机制解决了从哪里继续跑的问题但还有一个更隐蔽的问题恢复出来的旧状态是否还能和新上下文兼容。比如一个任务执行到一半用户追加了一条新需求此时直接恢复旧流程可能导致新需求被忽略或旧流程的输出与新需求冲突。我的处理方式是给每个任务状态加一个版本号。用户消息一旦变化系统会将状态版本号1并把新增需求作为上下文注入到后续节点同时在恢复后强制走一次规划校准检查——让规划Agent判断现有任务链路是否需要因新需求而调整。这个机制保证了恢复不是机械地继续而是“在保留已有成果的基础上重新对齐目标”。6. 幂等设计重复执行不再是事故幂等是可靠交付系统里最容易被忽略、但生产故障里最致命的问题之一。用户在界面上多点了一次按钮、网络请求重试、消息队列重复消费——任何一次重复触发如果系统没有幂等兜底就可能产生重复邮件、重复订单、重复扣款这类严重事故。6.1 哪些环节必须做幂等我总结了一套快速判断方法只要一个操作会产生外部副作用且该副作用无法自然地以结果为导向去重就必须做显式幂等。具体到伴AI系统有几个典型场景发送邮件/通知同一个任务被触发两次用户收到两封一模一样的邮件体验极差。文件写入/覆盖重复执行导致临时文件累积或目标文件被重复替换。API调用非查询类调用下游系统的创建、更新接口重复调用会产生重复数据。任务状态推进任务从DRAFT到EXECUTING再到DONE重复推进会导致状态机紊乱。6.2 幂等Key的生成策略幂等设计最关键的是幂等Key的生成。Key必须满足两个条件同一业务操作永远生成同一个Key不同业务操作几乎不可能生成相同Key。我的实践是采用三层构成任务类型 请求内容的语义哈希 触发来源标识。请求内容的语义哈希不是对原始字符串直接hash因为用户可能微调了几个字业务上仍然是同一意图。我对意图识别后的结构化结果做规范化序列化再算哈希这样帮我发周报和帮我把周报发一下会被识别为相同意图得到相同的幂等Key。# 幂等Key生成示例 import hashlib import json def generate_idempotency_key(task_type, intent_result, trigger_source): canonical json.dumps({ task_type: task_type, intents: intent_result[intents], params: intent_result[params] }, sort_keysTrue, ensure_asciiFalse) content_hash hashlib.sha256(canonical.encode(utf-8)).hexdigest()[:32] return f{task_type}:{content_hash}:{trigger_source}6.3 存储层的幂等去重生成幂等Key之后还需要一个存储层来做记录和判断。我的做法是在任务入库时加一个唯一索引unique index指向幂等Key。重复请求插入时会触发唯一约束冲突系统捕获冲突后直接将已有任务的状态返回给调用方而不是创建新任务。这里有一个容易被忽视的点判断幂等时不仅要能识别已经执行过还要能返回执行到哪一步了。比如用户重复提交了同一个任务第一次执行失败、停留在EXECUTING状态第二次提交如果直接返回已存在用户会觉得系统没有响应。所以我把幂等逻辑设计成命中幂等Key时返回当前任务的最新状态如果任务处于中间态允许调用方选择继续等待或触发恢复流程。幂等不仅在功能上与前述的LangGraph恢复机制产生了联动也更符合真实使用的直觉——用户真正关心的不是任务有没有重复创建而是我的事情到底办成了没有。6.4 外部系统调用层面的幂等兜底自己的系统做了幂等还不够还得考虑下游系统是否支持幂等。调用外部API时我会尽量在请求头中携带幂等键例如Idempotency-Key并在请求体中附带唯一业务标识。如果下游不支持幂等我会在本地保存一份外部副作用记录表记录对哪个下游系统、用了什么参数、期望产生什么效果这样即使下游返回超时不确定是否成功系统也可以通过查询侧结果来判断是否真的执行成功而不是盲目重试。例如发邮件这个操作调用发送接口超时后我们不会立刻重发而是先调用查询接口确认这封邮件是否已发出。如果已发出视为成功如果未发出才执行重试。这个先查询再重试的机制帮我们在实际运行中拦截了大量重复邮件事故。7. 结语可靠交付来自于对每一个失败环节的提前预判站在项目回望的角度我最大的体感是智能体系统真正的复杂性不在模型的推理能力而在推理之外的工程可靠性。用户看到的是从一句话到交付物的平滑体验背后却是意图识别、Agent编排、任务规划、质量评审、状态恢复、幂等控制一整套流水线的协作配合。几个我认为值得分享的经验总结意图识别阶段就要考虑识别错了怎么办而不是单纯追求准确率。多Agent的分工标准是职责边界不是模型数量。任务规划要拆到能独立验证、独立失败重试的原子粒度并且强副作用的操作要经过用户确认。评审要拆成规则引擎和LLM评审双轨硬指标不能交给大模型感觉去判断。LangGraph的检查点和恢复能力要从生产第一天就引入而不是等出故障再补。幂等Key要对语义做哈希而不是对原始文本做哈希才能覆盖真实场景中的说法多变性。这些机制的共同点都是把偶发问题变成可预期、可控制、可修复的流程分支。做一个可靠的Agent系统不是训练一个更聪明的模型而是把每一个可能出错的环节都提前设计好出错以后怎么办。希望这篇拆解对正在做类似项目的你有参考价值也欢迎在实际落地中针对你自己的场景做取舍——毕竟没有一套架构是通用的但可靠性设计的原则是共通的。
返回列表