news 2026/9/29 18:41:13

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
构建高效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还是Agent

2.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能可靠解析的格式。我踩过的坑:工具返回一个巨大的JSON,LLM只看了前几行就开始编。解决方案是:

  • 返回值做截断和摘要,超过一定长度的内容只返回关键字段+总数
  • 用明确的成功/失败标记,比如{"status": "success", "data": {...}},而不是直接返回data
  • 错误信息要可操作,不要返回“Error 500”,要返回“数据库连接超时,建议稍后重试或检查网络”
def search_knowledge_base(query: str, top_k: int = 5) -> dict: try: results = kb.search(query, limit=top_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的完整输入(system+context)和输出(包括工具调用)
  • 工具日志:工具名、参数、返回值、耗时、成功/失败
  • 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": "user@example.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_steps=15, token_budget=100000): 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, tools=self.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协作。先用单Agent+Workflow混合模式跑通一个真实场景,再逐步增加复杂度。我见过太多项目死在“架构太复杂,调不动”上。

构建高效AI Agent这件事,说到底是一个工程问题,不是算法问题。LLM的能力已经足够强,瓶颈在于我们怎么组织它的输入输出、怎么管理它的状态、怎么控制它的成本。把这几件事做好,一个“能跑通”的demo就能变成“能干活”的生产系统。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 18:40:10

YOLOv8s通道剪枝实战:BN稀疏化训练与TensorRT部署加速

1. 为什么要给yolov8s做剪枝&#xff1a;项目背景与方案选择1.1 yolov8s到底哪里“肥”了先从一个很实际的问题说起&#xff1a;yolov8s这个模型&#xff0c;官方给的数据是参数量大约11.2M&#xff0c;FP16精度下权重文件大概22MB左右。听着不算大&#xff0c;但真正跑到边缘设…

作者头像 李华
网站建设 2026/9/29 18:40:05

ROS1与ROS2无缝通信:用ros1_bridge打通Docker容器与主机

把 ROS2 容器和 ROS1 主机打通这件事&#xff0c;听起来像是要动大手术&#xff0c;实际上一套 ros1_bridge 就能搞定。你这边主机上跑着成熟的 ROS1 导航栈&#xff0c;那边容器里压着最新的 ROS2 算法包&#xff0c;两边各自为政确实浪费&#xff0c;让它们真正"对话&…

作者头像 李华
网站建设 2026/9/29 18:39:50

400G光模块测试进阶:从PCS层对齐标记到CMIS合规验证的完整指南

搞400G光模块测试&#xff0c;最怕的不是光学指标不过&#xff0c;而是PCS层偶尔丢一个AM、CMIS读寄存器突然超时这种“软故障”。前一种会让你在整机联调时抓破脑袋&#xff0c;后一种会在客户现场被一句“模块管理不正常”怼到哑口无言。这篇内容主要面向做光模块研发测试、交…

作者头像 李华
网站建设 2026/9/29 18:39:27

嵌入式偶发Bug排查指南:换机排除、录屏取证与批次对照实战

1. 偶发Bug为什么总是追查无果&#xff1a;先弄清“偶发”到底藏在哪里做嵌入式开发的人&#xff0c;基本都撞见过这种“偶尔来一回、换个设备又好了”的bug。串口偶发乱码丢帧、蓝牙断断续续掉线、烧录时不时的失败&#xff0c;几乎贯穿每个项目周期。碰到这类问题&#xff0c…

作者头像 李华
网站建设 2026/9/29 18:38:46

Java Web路灯管理系统:Servlet+JDBC轻量级实战项目

简介&#xff1a;这是一套面向计算机专业本科生的Java毕业设计完整实践资源&#xff0c;聚焦城市路灯管理信息化场景&#xff0c;采用B/S架构与JSPJava技术栈实现&#xff0c;适合课程设计、毕设选题及Java Web开发入门者系统学习。资源包共441个文件&#xff0c;7.75MB&#x…

作者头像 李华
网站建设 2026/9/29 18:38:34

Android手机模拟器运行PC与主机游戏:GTA5与血源诅咒实战指南

1. 手机变掌机这件事&#xff0c;到底靠不靠谱 第一次在Android手机上看到《GTA5》跑出接近60帧的画面时&#xff0c;我的反应和大多数人一样——这不会是录屏吧&#xff1f;直到自己亲手把一套完整流程跑通&#xff0c;看着洛圣都的街景在6.7寸屏幕上流畅滚动&#xff0c;才确…

作者头像 李华