ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

构建高效AI Agent:从Workflow编排到上下文工程与可观测性实战

构建高效AI Agent:从Workflow编排到上下文工程与可观测性实战 1. 为什么“能跑通”的Agent离“高效”还差十万八千里我见过太多团队做AI Agent的路径是这样的拿一个LLM的API Key写一个while循环把工具列表塞进system prompt跑通一个“查天气算数学”的demo然后兴冲冲地准备上生产。结果一上真实业务就崩了——要么是循环停不下来烧了几百刀token要么是工具调用参数永远对不上要么是同一个问题问三遍得到三个不同的答案。这个问题的根源在于大多数人把Agent当成了一个“更聪明的函数调用”而实际上Agent是一个需要被当作分布式系统来设计的运行时。它涉及状态管理、错误恢复、成本控制、可观测性、工具编排、上下文压缩等一系列工程问题。Anthropic在2024年底发布的《Building Effective Agents》里有一句话我特别认同最成功的Agent实现往往不是用了最复杂的框架而是用了最简单的可组合模式。这篇内容我想从零开始把“构建高效AI Agent”这件事拆开讲透。不是讲怎么调API而是讲为什么这样设计、什么场景该用什么模式、哪些坑我亲自踩过。适合已经跑通过demo但卡在“怎么让它稳定干活”阶段的开发者也适合正在做技术选型的架构师。全文会围绕Workflow编排、LLM选型、工具设计、上下文工程、可观测性这几个核心维度展开每个部分都会给出可复现的实操细节。先明确一个概念边界Agent和Workflow不是一回事。Workflow是预定义路径的编排LLM在固定节点做决策Agent是LLM自主决定路径和工具调用。Anthropic的工程博客里把这两者统称为“agentic systems”但强调了一个关键判断——能用Workflow解决的不要用Agent。因为Agent的自主性带来的是不可预测性而生产系统最怕的就是不可预测。我后面会详细讲这个决策树怎么画。2. 先想清楚你的场景到底该用Workflow还是Agent2.1 一个反直觉的判断标准看“路径是否可枚举”很多人一上来就问“用哪个Agent框架”但真正该问的是“我的任务路径能不能提前画出来”。我总结了一个简单的判断方法如果任务的步骤数量固定、顺序固定、每个步骤的输入输出格式固定那这就是一个Workflow用代码编排就行LLM只在需要理解自然语言的节点介入。如果任务的步骤数量不固定、下一步做什么取决于上一步的结果、需要动态选择工具那才需要Agent。如果任务介于两者之间比如大部分路径固定但偶尔需要“跳出去查个东西”那就用Workflow为主Agent为子节点的混合模式。举个例子一个“合同审核”任务。如果是“提取条款→比对模板→生成差异报告”这种固定三步那就是Workflow。但如果是“提取条款→发现某条款缺失→去知识库搜索相关法规→判断是否需要人工复核→生成报告”步骤数不固定这就是Agent场景。2.2 四种基础编排模式的实际代码结构Anthropic总结的四种模式——Prompt Chaining、Routing、Parallelization、Orchestrator-Workers——我在实际项目里都用过这里给出每种模式的最小实现思路和适用边界。Prompt Chaining提示链适合“每一步的输出是下一步的输入”的场景。比如“翻译→润色→格式化”。实现上就是一个顺序执行的函数列表每个函数的输出作为下一个的输入。关键点是每一步都要做校验如果中间某步输出格式不对要能中断并重试而不是把错误一路传下去。Routing路由适合“根据输入类型分派到不同处理逻辑”的场景。比如客服系统里先判断用户问题是“退款”“技术故障”还是“咨询”然后走不同的处理链。实现上是一个分类器可以用小模型或规则多个处理分支。这里的关键是分类器的准确率要足够高否则路由错了后面全错。我的经验是分类任务用便宜的小模型就够了没必要上大模型。Parallelization并行化有两种子模式一种是“分片并行”把大任务切成小块同时处理再合并比如长文档摘要另一种是“投票并行”同一个任务跑多次取多数结果用于提高准确性。实现上用asyncio.gather或者线程池就行。注意点是并行任务的失败处理——如果10个分片里有1个失败是整体失败还是降级返回部分结果这个策略要提前定。Orchestrator-Workers编排者-工作者是最接近“Agent”的模式一个主LLM负责拆解任务和分派多个子LLM负责执行。适合“任务复杂度高、无法提前确定子任务”的场景比如“帮我调研某个技术方案”。实现上主LLM输出一个任务列表然后动态创建子任务执行。这里最大的坑是子任务的输出如何汇总——如果子任务返回的是自由文本主LLM很难可靠地整合。我的做法是强制子任务返回结构化JSON主LLM只做拼接和去重。2.3 什么时候必须上Agent三个信号信号一工具数量超过10个。当工具多到无法在prompt里全部描述清楚时需要Agent动态检索和选择工具。信号二任务步骤数方差大。有的请求3步搞定有的要30步Workflow没法覆盖。信号三需要自我纠错。比如代码生成后要跑测试失败了要自己改这种循环反馈只有Agent能做。但即使上了Agent我也强烈建议给Agent加护栏最大循环次数我一般设15-20、单次会话token预算、工具调用白名单。没有护栏的Agent就是一颗定时炸弹。3. 工具设计才是Agent能力的真正天花板3.1 工具描述写不好再强的LLM也白搭我做过一个对比实验同一套工具一组用“查询天气”这种模糊描述一组用详细的参数说明示例边界条件任务成功率差了将近40%。LLM选择工具完全依赖描述文本描述就是工具的“API文档”而且是给一个“没耐心、容易误解、不会追问”的开发者看的。一个好的工具描述应该包含功能一句话概括、每个参数的类型和含义、什么情况下该用这个工具、什么情况下不该用、一个调用示例。比如# 差的描述 { name: search, description: 搜索信息, parameters: {query: string} } # 好的描述 { name: search_knowledge_base, description: 在企业内部知识库中搜索文档。适用于查询产品文档、流程规范、历史工单。不适用于查询实时数据用get_realtime_data或外部网页用web_search。, parameters: { query: { type: string, description: 搜索关键词建议使用名词短语不要用完整句子。例如退款流程而不是我想知道怎么退款 }, top_k: { type: integer, description: 返回结果数量默认5最大20。结果太多会稀释相关性 } } }3.2 工具粒度太粗和太细都是灾难工具粒度设计有个“金发姑娘原则”不能太粗一个工具干所有事LLM不知道怎么传参也不能太细几十个原子工具LLM选择困难。我的经验法则是一个工具对应一个“用户能理解的操作”。比如“发邮件”这个功能不要设计成create_smtp_connection、set_recipient、set_body、send四个工具也不要设计成do_everything一个工具。正确的是send_email(to, subject, body, attachments)一个工具内部处理连接、认证、发送。另一个经验读操作和写操作要分开。读操作查询、搜索可以宽松一点写操作删除、修改、发送要加确认机制。我见过Agent把“查询用户”理解成“删除用户”的惨案就是因为工具命名太接近且没有确认步骤。3.3 工具返回值的结构化处理工具返回给LLM的内容必须是LLM能可靠解析的格式。我踩过的坑工具返回一个巨大的JSONLLM只看了前几行就开始编。解决方案是返回值做截断和摘要超过一定长度的内容只返回关键字段总数用明确的成功/失败标记比如{status: success, data: {...}}而不是直接返回data错误信息要可操作不要返回“Error 500”要返回“数据库连接超时建议稍后重试或检查网络”def search_knowledge_base(query: str, top_k: int 5) - dict: try: results kb.search(query, limittop_k) return { status: success, count: len(results), results: [ {title: r.title, snippet: r.text[:200], id: r.id} for r in results ], hint: 如需查看完整内容用get_document(id) } except TimeoutError: return { status: error, error_type: timeout, message: 知识库响应超时, suggestion: 可以缩小查询范围或稍后重试 }这种结构化返回让LLM能明确知道“成功了没有”“有多少结果”“下一步能干什么”而不是对着一坨文本瞎猜。4. 上下文工程决定Agent能不能跑长任务的关键4.1 上下文窗口不是越大越好很多人觉得“上下文窗口大能力强”实际上上下文越长LLM的注意力越分散。我做过测试同样一个任务把无关的历史对话塞进去准确率下降15%以上。所以高效Agent的核心能力之一是主动管理上下文而不是无脑堆token。我的上下文管理策略分三层第一层系统提示词精简。只放角色定义、核心规则、工具列表。不要放示例对话few-shot示例放在工具描述里更有效。系统提示词控制在2000 token以内。第二层对话历史压缩。当历史超过一定轮数我一般设10轮把早期对话用LLM总结成一段“到目前为止的进展”替换掉原始对话。总结的prompt要明确要求保留已确认的事实、已完成的步骤、待解决的问题。第三层工具结果截断。工具返回的长文本只保留与当前任务相关的部分。比如搜索返回10个文档只把最相关的3个的摘要放进上下文其余的存在外部存储里需要时再取。4.2 用“工作记忆”替代“全量历史”一个很有效的模式是维护一个结构化的scratchpad而不是把全部对话历史塞进上下文。scratchpad里只放当前任务目标一句话已完成步骤列表每步一行关键发现事实性信息待办事项每次调用LLM时system prompt scratchpad 最近2-3轮对话就够了。这样上下文长度可控而且LLM的注意力集中在真正重要的信息上。scratchpad { goal: 帮用户找到2024年Q3的销售数据并生成趋势分析, completed: [ 确认了数据在sales_db.prod.quarterly表中, 查询到Q3总销售额为1.2亿 ], findings: [ Q3环比增长8%但9月单月下降3%, 华东区贡献了45%的销售额 ], todos: [ 获取月度明细数据, 生成趋势图 ] }这个scratchpad每次LLM调用后更新作为下一轮的上下文。实测下来比全量历史的方式token消耗降低60%任务成功率反而更高。4.3 长任务的“检查点”机制对于可能跑很多步的任务我会在关键节点做检查点把当前状态序列化存下来。如果后续步骤失败可以从检查点恢复而不是从头再来。这个机制在调试阶段特别有用——你可以从任意检查点重放快速定位是哪一步出了问题。检查点的存储用简单的JSON文件就行不需要上数据库。关键是检查点要包含足够恢复状态的信息scratchpad、已调用的工具及结果、当前循环计数。5. 模型选型与成本控制不是所有节点都需要大模型5.1 分层用模型贵的用在刀刃上一个高效Agent系统里不应该所有LLM调用都用同一个模型。我的分层策略路由/分类节点用最便宜的小模型如Haiku级别因为任务简单只需要判断意图。工具参数生成用中等模型如Sonnet级别需要一定的理解能力但不需要深度推理。复杂推理/规划用最强模型如Opus级别只在Orchestrator的规划步骤用。结果汇总/格式化用中等模型或小模型因为输入已经结构化只需要整理。这样下来整体成本能降低70%以上而效果几乎无损。关键是要明确每个节点的能力需求不要一刀切。5.2 Token预算的硬控制我在每个Agent会话里都会设一个token预算比如10万token。每次LLM调用前检查已消耗量超过80%就触发“收尾模式”——让LLM用最少的步骤完成任务或返回当前进展。超过100%直接终止并返回部分结果。这个机制听起来简单但能避免很多“Agent陷入循环烧光预算”的事故。实现上就是在调用LLM的封装函数里加一个计数器。5.3 缓存策略相同输入不重复调用Agent场景里有很多重复调用同一个工具用相同参数查两次、同一个分类任务反复做。加一层语义缓存能省不少钱。简单做法是用(model, prompt_hash)做key命中直接返回。进阶做法是用embedding做语义相似度匹配相似度超过阈值就复用结果。但要注意写操作不能缓存。查询可以缓存删除/修改/发送这类操作必须每次真实执行。6. 可观测性没有日志的Agent就是黑盒6.1 必须记录的五个维度一个Agent跑完我必须能回答这些问题它每一步做了什么决策为什么选这个工具每次LLM调用的输入输出是什么消耗了多少token总耗时多少所以日志里必须包含决策日志每轮LLM的完整输入systemcontext和输出包括工具调用工具日志工具名、参数、返回值、耗时、成功/失败Token日志每次调用的prompt token、completion token、累计时间日志每步的开始/结束时间戳状态日志scratchpad的每次变更这些日志用结构化JSON输出方便后续分析和回放。我一般用简单的文件日志一个查看脚本不需要上复杂的可观测性平台。6.2 回放与调试把Agent的“思考过程”可视化调试Agent最有效的方法是回放。把一次会话的所有日志按时间顺序展开你能清楚看到LLM在第3步选错了工具导致第4步参数不对第5步开始胡编。没有回放你只能看到最终失败根本不知道哪里出的问题。我写了一个简单的回放脚本把日志渲染成可读的时间线[Step 1] LLM调用 (1.2s, 450 tokens) → 决策: 调用 search_knowledge_base → 参数: {query: 退款流程, top_k: 5} [Step 2] 工具调用 (0.8s) → 返回: 3条结果 → 状态更新: findings 退款需要3-5个工作日 [Step 3] LLM调用 (2.1s, 890 tokens) → 决策: 调用 send_email → 参数: {to: userexample.com, ...} → 警告: 写操作未确认这个回放让我在几分钟内定位问题而不是靠猜。6.3 关键指标监控生产环境里我会监控这几个指标任务成功率完成/总数、平均步数步数突然增加说明有问题、平均token消耗成本、工具调用失败率哪个工具不稳定、循环终止率多少任务是因为达到最大循环次数而终止的。这些指标异常时能第一时间发现。7. 从Demo到生产我踩过的五个真实坑7.1 坑一工具调用参数类型不匹配LLM生成的参数经常是字符串但工具期望整数。比如top_k: 5而不是top_k: 5。解决方案是在工具封装层做类型强制转换和校验用pydantic之类的库定义参数schema不匹配就返回明确的错误让LLM重试。7.2 坑二LLM“假装”调用了工具有时候LLM会在文本里写“我将调用search工具”但实际上没有产生tool_call。这通常是因为工具描述和prompt格式不匹配。解决方案是用模型原生的function calling格式不要自己解析文本。如果模型不支持原生function calling那就要在prompt里非常明确地规定输出格式并加校验。7.3 坑三无限循环Agent反复调用同一个工具因为每次返回的结果它都觉得“不够好”。解决方案是加循环检测如果连续3次调用同一个工具且参数相似强制中断并返回当前结果。另外在prompt里明确“如果工具返回了结果就基于结果继续不要重复调用”。7.4 坑四上下文污染早期对话里的错误信息被LLM当成事实后续一直基于错误信息推理。解决方案是scratchpad只记录确认过的事实工具返回的错误信息要标记为“待验证”不要让LLM直接采信。7.5 坑五工具副作用不可逆Agent执行了删除操作但用户其实只是想查询。解决方案是写操作必须二次确认Agent生成一个“待执行操作”的描述让用户确认后再执行。或者用“软删除”机制先标记删除给一个撤销窗口。8. 一个最小可用的高效Agent骨架把上面的东西串起来一个高效Agent的核心结构大概是这样class EfficientAgent: def __init__(self, llm_client, tools, max_steps15, token_budget100000): self.llm llm_client self.tools {t.name: t for t in tools} self.max_steps max_steps self.token_budget token_budget self.scratchpad {goal: , completed: [], findings: [], todos: []} self.step_count 0 self.token_used 0 def run(self, user_input): self.scratchpad[goal] user_input while self.step_count self.max_steps: if self.token_used self.token_budget * 0.8: return self._wrap_up() context self._build_context() response self.llm.call(context, toolsself.tools) self.token_used response.usage.total_tokens self.step_count 1 if response.is_final: return response.content if response.tool_call: result self._execute_tool(response.tool_call) self._update_scratchpad(response.tool_call, result) return self._wrap_up() def _build_context(self): return { system: SYSTEM_PROMPT, scratchpad: self.scratchpad, recent: self.recent_messages[-3:] }这个骨架不依赖任何框架纯Python就能跑。核心思想是状态外置scratchpad、预算硬控token_budget、步数限制max_steps、工具封装_execute_tool里做校验和错误处理。框架能帮你省一些代码但理解这些机制比会用框架重要得多。9. 关于框架选型的一点个人看法LangChain、LlamaIndex、Dify这些框架我都用过。我的感受是框架适合快速验证不适合直接上生产。原因有三一是框架的抽象层太厚出问题时很难定位二是框架的更新频率高今天能跑的代码下个月可能就breaking change三是框架的默认行为不一定适合你的场景比如默认的上下文管理策略可能很浪费token。我的建议是用框架做原型用原生代码做生产。原型阶段用框架快速验证想法确认可行后把核心逻辑用原生代码重写只保留真正需要的部分。这样你对系统的控制力最强也最容易优化。如果非要用框架选那些轻量、可组合的比如Anthropic的SDK本身就很干净或者用OpenAI的function calling 自己的编排逻辑。重框架什么都要管的在Agent场景里往往是负担。10. 最后分享几个实操小技巧技巧一给工具加“使用示例”。在工具描述里加一个example字段展示一个典型的调用。LLM看到示例后参数正确率明显提升。技巧二用“思考-行动”格式。在prompt里要求LLM先输出一段简短的思考“我需要先查一下...”再输出工具调用。这个思考过程不一定要展示给用户但能显著提高决策质量。技巧三错误重试要带上下文。工具调用失败后重试时把错误信息一起传给LLM让它调整参数。不要简单重试相同参数。技巧四定期review日志。我每周会抽10条失败case做回放分析往往能发现系统性的问题比如某个工具描述有歧义、某类任务总是超步数。技巧五从最简单的模式开始。不要一上来就搞多Agent协作。先用单AgentWorkflow混合模式跑通一个真实场景再逐步增加复杂度。我见过太多项目死在“架构太复杂调不动”上。构建高效AI Agent这件事说到底是一个工程问题不是算法问题。LLM的能力已经足够强瓶颈在于我们怎么组织它的输入输出、怎么管理它的状态、怎么控制它的成本。把这几件事做好一个“能跑通”的demo就能变成“能干活”的生产系统。
返回列表