1. 从"手搓循环"到"框架化":Agent开发绕不开的那道坎
如果你最近在折腾AI Agent,大概率经历过这个阶段:一开始觉得Agent特别简单,不就是"让大模型调用工具,拿到结果再喂回去,循环几轮"嘛。于是你兴冲冲地写了个while循环,把工具描述塞进提示词,跑通了天气查询、计算器、网页搜索这几个demo,心里美滋滋。然后你试着加第二个工具、第三个工具,加上多轮对话记忆,加上错误重试,加上超时控制……代码开始失控,提示词越写越长,模型时不时抽风不按格式输出,你花在"修框架"上的时间远超"做业务"的时间。
这就是"手搓Agent"的典型困境。而Agent范式的框架化实现,本质上就是解决这个问题:把ReAct这类"思考-行动-观察"的循环模式,从散落在业务代码里的临时逻辑,抽象成一套可复用、可扩展、可调试的工程结构。关键词里的SimpleAgent、ReAct、提示词工程,其实指向的是同一件事——如何用最小的抽象成本,把Agent的"思考与行动"能力封装成一个稳定的骨架。
这篇文章不打算给你灌一堆概念,而是从我自己反复重构Agent代码的经历出发,讲清楚框架化到底在框架化什么、SimpleAgent这种极简实现为什么值得先吃透、ReAct循环的每一步在工程上对应哪些坑、提示词工程在框架里应该放在哪一层。适合已经跑通过至少一个Agent demo、但被代码混乱折磨过的开发者,也适合想理解"Agent框架图"背后设计逻辑的进阶读者。读完你至少能自己动手搭一个结构清晰、能扛住多工具多轮次的Agent骨架。
2. 框架化到底在解决什么问题:拆解Agent的四个不稳定源
在动手写框架之前,得先想明白一件事:为什么Agent代码这么容易乱?我的经验是,Agent系统里有四个天然的"不稳定源",框架化的核心目标就是把这四个源逐个隔离、收敛。
2.1 不稳定源一:模型输出的格式漂移
大模型最让人头疼的地方在于,它不是一个确定性的函数。你让它输出JSON,它这次给你标准JSON,下次给你包在```json代码块里,再下次给你加一句"好的,以下是结果:"。在手搓代码里,这种漂移会直接导致解析崩溃,然后整个循环挂掉。
框架化的第一个动作,就是把"模型原始输出"和"结构化动作"之间加一层解析适配器。这层适配器要能容忍常见的格式变体:去掉markdown代码块标记、提取第一个合法JSON对象、处理前后多余的自然语言。我在实际项目里踩过的坑是,早期直接在业务代码里写json.loads(response),结果线上因为模型偶尔多输出一个逗号就报错。后来把这层解析独立出来,加上正则兜底和重试,稳定性立刻上了一个台阶。
2.2 不稳定源二:工具调用的边界模糊
手搓Agent时,工具往往就是几个函数,参数校验、异常处理、返回值格式全靠约定。工具一多,就会出现"这个工具返回字符串,那个工具返回字典,还有一个返回列表"的混乱局面,模型拿到不同格式的观察结果,推理质量直线下降。
框架化要求每个工具都有统一的契约:明确的名称、描述、参数schema、返回结构。这不是为了好看,而是因为模型是"看描述干活"的,工具描述的质量直接决定它会不会用错工具。我见过太多案例,模型调错工具不是因为能力不行,而是因为工具描述写得含糊,比如两个工具都叫"搜索",一个搜网页一个搜数据库,模型当然懵。
2.3 不稳定源三:循环终止条件的失控
ReAct循环最危险的地方是它可能"停不下来"。模型可能反复调用同一个工具,或者陷入"思考-行动-思考-行动"的死循环,token哗哗地烧。手搓代码里,终止条件往往就是一个max_iterations计数器,但这远远不够。
框架化需要设计多层终止策略:迭代次数上限、单次任务超时、连续重复动作检测、以及模型主动输出"最终答案"的识别。这里有个经验:连续重复动作检测特别有用,如果模型连续两次调用完全相同的工具和参数,基本可以判定它卡住了,直接中断比让它继续烧token划算得多。
2.4 不稳定源四:上下文膨胀与记忆管理
每一轮循环都会往上下文里追加"思考、行动、观察"三段内容,几轮下来上下文就爆了。手搓代码通常不管这个,直接把所有历史塞进去,结果要么超长被截断,要么token成本失控。
框架化要把上下文管理作为一等公民:哪些历史必须保留(比如工具调用的关键结果),哪些可以压缩(比如冗长的思考过程),哪些可以丢弃(比如已经完成的子任务细节)。这块我在后面会专门展开讲,因为它直接决定了Agent能不能处理长任务。
把这四个源隔离好,你的Agent骨架就立起来了。接下来讲SimpleAgent,它是理解这一切的最佳起点。
3. SimpleAgent:为什么"极简"反而是最好的学习入口
很多人一上来就想搞多Agent协作、搞复杂的编排引擎,结果连单Agent的循环都没吃透。我的建议恰恰相反:先把SimpleAgent写到极致简单,简单到你能一眼看穿每一行代码在干什么,然后再往上加东西。SimpleAgent不是"简陋版",而是"去掉了所有非必要复杂度"的最小可用骨架。
3.1 SimpleAgent的最小构成:三个组件加一个循环
一个能跑的SimpleAgent,核心就三样东西:
- 一个LLM调用封装:负责把消息列表发给模型,拿回文本。别小看这一层,它要处理重试、超时、token统计。
- 一组工具定义:每个工具有名称、描述、参数schema、执行函数。
- 一个提示词模板:告诉模型它是谁、有哪些工具、输出格式是什么。
- 一个循环:调用模型 → 解析输出 → 如果是动作就执行工具 → 把结果追加回上下文 → 再调用模型,直到模型给出最终答案或触发终止条件。
就这么简单。我见过有人把SimpleAgent写成几百行的"迷你框架",加了插件系统、事件总线、中间件,结果调试的时候自己都绕晕。极简的价值在于可调试性:当Agent行为异常时,你能快速定位是提示词问题、解析问题还是工具问题,而不是在一堆抽象层里迷路。
3.2 提示词模板:框架里最容易被低估的一层
在SimpleAgent里,提示词模板承担了"协议"的角色。它必须清楚地告诉模型三件事:可用工具及其格式、期望的输出格式、以及"什么时候该停止"。
我常用的模板结构是这样的(这是基于常见ReAct实践的补充):
你是一个可以调用工具的智能助手。你可以使用以下工具: {tool_descriptions} 请严格按照以下格式回应: 思考:<你的推理过程> 行动:<工具名称> 行动输入:<工具参数的JSON> 当你已经可以回答用户问题时,使用以下格式: 思考:<你的推理过程> 最终答案:<你的回答>这个模板的关键在于格式的确定性。模型只要照着"思考/行动/最终答案"的标签走,解析层就能稳定提取。我踩过的坑是早期用自然语言描述格式,比如"请先思考再决定是否调用工具",结果模型输出五花八门,解析层疲于奔命。用标签化的硬格式,比用自然语言描述格式,稳定性高一个数量级。
3.3 工具描述怎么写,模型才不犯错
工具描述是提示词工程里最见功力的部分。我的经验是遵循三条原则:
第一,描述要写"什么时候用",而不只是"是什么"。比如"计算器"这个工具,与其写"用于数学计算",不如写"当需要精确计算数字表达式时使用,不要自己心算"。
第二,参数描述要给出示例。模型对示例的敏感度远高于抽象描述。参数schema里带上"example": "北京"这样的字段,调用准确率明显提升。
第三,工具数量控制在合理范围。我实测下来,单Agent的工具数量超过10个后,模型选错工具的概率显著上升。如果业务确实需要很多工具,考虑分组或者用路由层先做一轮筛选。
3.4 一个能跑的最小循环实现
把上面几块拼起来,SimpleAgent的核心循环大概长这样(Python示意):
def run_agent(user_input, tools, max_iterations=8): messages = build_initial_messages(user_input, tools) for i in range(max_iterations): response = call_llm(messages) parsed = parse_response(response) if parsed.type == "final_answer": return parsed.content if parsed.type == "action": observation = execute_tool(parsed.tool_name, parsed.tool_input, tools) messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": f"观察:{observation}"}) else: # 解析失败,追加纠错提示 messages.append({"role": "user", "content": "格式错误,请严格按格式重新输出。"}) return "达到最大迭代次数,任务未完成。"这段代码不到20行,但它包含了框架化的所有核心要素:循环、解析、工具执行、上下文追加、终止条件。你能把这20行讲清楚,就理解了Agent框架的骨架。后面所有的复杂框架,无非是在这20行上做加法和抽象。
4. ReAct循环的工程化:每一步都藏着坑
ReAct(Reasoning + Acting)是Agent范式的理论核心,但理论落到工程上,每一步都有细节。这一章我按循环的实际执行顺序,把每个环节的坑和应对讲透。
4.1 思考阶段:如何让模型"想得对"
思考阶段是模型生成推理文本的过程。这里最大的坑是模型跳过思考直接行动,或者思考内容空洞(比如"我需要调用工具"这种废话)。
应对方法有两个。一是在提示词里强制思考的深度,比如要求"思考部分必须说明为什么选择这个工具、预期得到什么结果"。二是用few-shot示例,在提示词里给一两个高质量的思考范例,模型会模仿这个风格。我实测下来,加了示例后,思考质量提升非常明显,尤其是多步推理任务。
还有一个细节:思考内容要不要保留在上下文里?我的做法是保留但压缩。完整的思考对后续推理有帮助,但太长的思考会挤占上下文。可以在追加时对超过一定长度的思考做摘要,或者只保留最近几轮的完整思考。
4.2 行动阶段:工具选择的准确率优化
行动阶段的核心指标是工具选择准确率。影响它的因素按重要性排序:工具描述质量 > 工具数量 > 提示词格式 > 模型能力。
我做过一个对比实验,同样的工具集,只优化描述(补充使用场景和示例),工具选择准确率从七成出头提升到九成以上。这说明大部分"模型选错工具"的问题,其实是描述没写好。
另一个技巧是给工具加"负向描述",明确说什么时候不该用。比如"网页搜索工具:当需要最新信息时使用;如果问题涉及本地文件,请使用文件读取工具"。这种排除法能有效减少误选。
4.3 观察阶段:工具返回结果的处理艺术
工具执行完返回的观察结果,直接决定模型下一轮推理的质量。这里有几个坑:
坑一:返回结果太长。比如网页搜索返回一整页HTML,塞进上下文既浪费token又干扰推理。我的做法是在工具层做结果裁剪,只返回最相关的片段,或者做一次摘要。
坑二:返回结果格式不统一。有的工具返回JSON,有的返回纯文本,模型处理起来容易混乱。框架层应该统一观察结果的包装格式,比如都包成{"status": "success", "data": ...}或{"status": "error", "message": ...}。
坑三:错误信息不友好。工具报错时,如果直接把堆栈信息丢给模型,模型完全无法理解。应该把错误翻译成模型能懂的自然语言,比如"文件不存在,请检查路径"而不是"FileNotFoundError at line 42"。
4.4 循环控制:终止、重试与降级
循环控制是框架的"安全阀"。除了前面说的迭代上限和超时,还有几个实用策略:
重复动作检测:记录最近几次的工具调用签名(工具名+参数),如果连续重复,直接中断并返回当前最佳结果。
解析失败重试:解析失败时不要立刻放弃,追加一条纠错提示让模型重试,通常重试一两次就能恢复。但要设重试上限,避免死循环。
降级策略:当循环达到上限仍未完成时,不要直接返回错误,而是让模型基于已有信息给出一个"尽力而为"的答案。用户体验上,一个不完美但有用的答案,远好过一个冷冰冰的报错。
4.5 一个完整的ReAct执行轨迹示例
假设用户问"帮我查一下今天北京的天气,然后换算成华氏度"。一个健康的执行轨迹是这样的:
思考:用户需要北京今天的天气,并换算成华氏度。我需要先查天气,再做温度换算。 行动:get_weather 行动输入:{"city": "北京", "date": "today"} 观察:{"status": "success", "data": {"temp_c": 25, "condition": "晴"}} 思考:北京今天25摄氏度,晴天。现在需要把25摄氏度换算成华氏度,使用换算工具。 行动:celsius_to_fahrenheit 行动输入:{"celsius": 25} 观察:{"status": "success", "data": {"fahrenheit": 77}} 思考:已经拿到所有信息,可以给出最终答案。 最终答案:北京今天晴天,气温25摄氏度,约合77华氏度。这个轨迹清晰、每步都有理由、工具调用准确。框架化的目标就是让这种健康轨迹成为常态,而不是靠运气。
5. 提示词工程在框架中的分层落地
提示词工程不是"写一段好提示词"这么简单,在框架化实现里,它应该被分层管理。我把它分成四层,每层职责不同,改动时互不干扰。
5.1 系统层:定义Agent的身份与总规则
系统层提示词定义Agent是谁、能力边界在哪、总体的行为准则。这部分相对稳定,不随任务变化。比如"你是一个严谨的助手,不确定的信息要明确说明,不要编造"。系统层还应该包含安全边界,比如拒绝执行某些类型的操作。
系统层的坑是写得太啰嗦。我见过系统提示词写了上千字,结果模型注意力被稀释,关键规则反而记不住。系统层控制在几百字以内,只放最核心的身份和规则。
5.2 工具层:动态注入的工具说明
工具层提示词是根据当前可用工具动态生成的。这一层要解决的是"模型怎么知道有哪些工具、怎么用"。前面讲过工具描述的原则,这里补充一点:工具层应该支持按场景动态裁剪。比如一个客服Agent,在售前场景只注入商品相关工具,在售后场景只注入订单相关工具,减少干扰。
5.3 格式层:约束输出结构的协议
格式层是保证解析稳定的关键。它明确规定模型输出的标签、字段、顺序。这一层要极其严格,用标签化的硬格式,避免任何歧义。格式层还应该包含错误示例,告诉模型"不要这样输出",比如"不要输出markdown代码块包裹的JSON"。
5.4 任务层:针对具体任务的补充指令
任务层是每次调用时根据用户输入动态生成的。比如用户问的是数据分析任务,任务层可以补充"优先使用统计工具,注意数据的单位"。这一层是灵活性的来源,让同一个Agent骨架能适配不同任务。
四层分开管理的好处是可维护性:改工具不影响格式,改格式不影响身份。我在实际项目里,把提示词按这四层拆成独立文件后,调试效率提升了一大截,因为每次只需要关注出问题的那一层。
6. 从SimpleAgent到可扩展骨架:抽象边界的把握
SimpleAgent跑通后,你会自然想加东西:加记忆、加多Agent、加人工介入。这时候抽象边界的把握就成了关键——加得太少不够用,加得太多又回到"框架地狱"。
6.1 什么时候该抽象,什么时候该硬编码
我的判断标准是:同一个逻辑出现第三次时,才考虑抽象。第一次写死,第二次复制,第三次才提取成通用组件。过早抽象是Agent框架开发里最常见的错误,因为Agent的需求变化太快,你抽象出来的接口很可能下一周就不适用了。
比如工具执行的重试逻辑,如果只有一两个工具需要重试,直接在工具里写就行;当大部分工具都需要统一的重试策略时,再提取到框架层。
6.2 记忆模块的接入点
记忆是Agent从"无状态"走向"有状态"的关键。接入记忆的时机是当你发现Agent需要跨会话记住信息时。记忆模块的设计要点:
- 短期记忆:当前会话的上下文,就是消息列表本身。
- 长期记忆:跨会话的信息,通常用向量库存储,按相关性检索后注入上下文。
- 工作记忆:当前任务的关键中间结果,可以单独维护一个结构化的"任务状态"对象。
我踩过的坑是过早引入向量库,结果检索质量不稳定,反而干扰了推理。后来改成"先用结构化的工作记忆,确实需要语义检索时再上向量库",简单可靠得多。
6.3 多Agent协作的引入时机
多Agent不是必须的。单Agent能解决的问题,不要用多Agent。多Agent的价值在于职责隔离:当不同子任务需要完全不同的工具集和提示词时,拆成多个Agent比塞进一个Agent更清晰。
引入多Agent的典型信号是:单Agent的工具数量超过15个、提示词超过2000字、或者不同任务的输出格式差异巨大。这时候可以考虑"路由Agent + 专家Agent"的结构,路由Agent负责分派,专家Agent各司其职。
6.4 可观测性:框架必须内建的调试能力
框架化最容易被忽略但最重要的部分,是可观测性。一个没有日志、没有追踪的Agent框架,出问题时你只能靠猜。
我的做法是每一步都记录结构化日志:每轮循环的输入消息、模型原始输出、解析结果、工具调用及耗时、终止原因。这些日志在调试时价值连城。更进一步,可以做一个简单的执行轨迹可视化,把每轮的思考-行动-观察按时间线展示出来,一眼就能看出Agent在哪一步跑偏了。
7. 实测中的那些坑与应对经验
理论讲完了,这一章全是实战里踩出来的经验,可能比前面所有内容都值钱。
7.1 模型"假装"调用了工具
这是最隐蔽的坑之一。模型在思考里写"我将调用天气工具",然后直接给出一个编造的天气结果,根本没有真正输出行动指令。解析层如果只检查"最终答案",就会把这个编造结果当成真的返回给用户。
应对方法是严格的状态机校验:只有真正执行过工具、拿到过观察结果,才允许输出最终答案。如果模型在没有任何工具调用的情况下直接给最终答案,而任务明显需要工具,就追加提示"你还没有调用工具,请先调用工具获取信息"。
7.2 参数格式的隐性错误
模型调用工具时,参数格式经常出问题:该传数字的传了字符串、该传数组的传了单个值、日期格式五花八门。这些错误在工具执行时才暴露,但错误信息往往不友好。
我的做法是在工具层做参数预处理和校验:用schema做类型转换(能转就转),转换失败时返回明确的错误提示,告诉模型"参数X应该是数字,你传的是字符串,请修正"。这种带纠错指引的错误返回,能让模型在下一轮自我修复。
7.3 上下文被工具结果淹没
当一个工具返回大量数据时,后续几轮的推理质量会明显下降,因为模型被无关信息干扰了。我遇到过一个案例,网页搜索返回了整页内容,模型在后续推理里反复引用页面里的广告文字。
应对方法是工具层做结果摘要:对于长结果,先用一次轻量的LLM调用做摘要,只把摘要注入上下文,完整结果存在外部供需要时查询。这个"摘要-引用"模式在处理长文档时特别有效。
7.4 并发场景下的状态污染
如果你的Agent服务要处理并发请求,一定要注意状态隔离。我早期犯过一个错误,把消息列表存在了全局变量里,结果两个并发请求互相污染上下文,输出完全错乱。每个请求必须有独立的会话状态对象,这是并发安全的基本要求。
7.5 token成本的隐性失控
Agent的token消耗远高于普通对话,因为每轮循环都要把完整上下文发一遍。一个8轮的循环,token消耗可能是单次对话的十几倍。控制成本的手段:压缩历史上下文、限制工具结果长度、合理设置迭代上限、对简单任务用更小的模型。我实测下来,把工具结果长度限制在合理范围,能省下相当可观的成本。
8. 写在最后:框架是手段,不是目的
折腾了这么多Agent框架,我最大的体会是:框架化的目的是让你把精力放在业务逻辑上,而不是框架本身。如果你发现自己在框架上花的时间比业务还多,那大概率是抽象过度了。
SimpleAgent这种极简实现,其实能覆盖相当一部分实际需求。真正需要复杂框架的场景,往往是多工具、多轮次、多Agent协作的重型任务。所以我的建议是:从SimpleAgent起步,遇到具体问题再加具体能力,让框架跟着需求长,而不是先搭一个大框架再往里塞需求。
最后分享一个我常用的调试技巧:当Agent行为异常时,先把完整的执行轨迹打印出来,逐轮检查"思考是否合理、行动是否匹配、观察是否被正确理解"。十有八九,问题就出在某一轮的观察结果处理上——要么太长、要么格式乱、要么错误信息不友好。把这一轮修好,整个Agent就顺了。这个排查思路,比任何框架都管用。