ARTICLE DETAIL

资讯详情

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

Agent范式框架化:SimpleAgent与ReAct循环的工程实践

Agent范式框架化:SimpleAgent与ReAct循环的工程实践 1. 从一堆散装代码到可复用框架Agent 范式到底在解决什么问题这两年做 AI 应用的人应该都有同感写一个能跑起来的 Agent Demo 可能只需要一个下午但要把这套东西变成团队里其他人也能用、能改、能扩展的东西往往要花上好几周。问题不在于模型能力不够而在于大多数人一开始就是面向 Demo 编程——提示词硬编码在函数里工具调用逻辑和业务逻辑搅在一起换个场景就得推倒重来。我最早接触 Agent 开发的时候也是这样一个 Python 文件里塞了三百多行里面有提示词模板、有工具注册、有循环控制、有结果解析看起来什么都有实际上什么都改不动。后来踩了几次坑才明白Agent 开发真正难的不是让它跑起来而是让它可维护、可复用、可组合。这就是 Agent 范式框架化要解决的核心问题。所谓Agent 范式框架化说白了就是把 Agent 运行过程中那些反复出现的模式——思考、行动、观察、再思考——抽象成一套稳定的结构让开发者只需要关注这个 Agent 要做什么而不是这个循环该怎么写。这里面涉及几个关键概念SimpleAgent是最小可运行单元ReAct是思考与行动交替的核心范式提示词工程则是把意图准确传达给模型的手段。三者组合起来才构成一个能落地的 Agent 框架。这篇文章适合谁看如果你已经写过一两个 Agent Demo但总觉得代码乱、不好扩展那这篇内容会对你有帮助。如果你还没动手写过只是想了解 Agent 框架的设计思路也能从里面看到一套完整框架是怎么从零搭起来的。我会用从业者的视角把框架化实现的每个环节拆开讲包括为什么这么设计、参数怎么定、坑在哪里。2. 框架化设计的核心思路与方案选型2.1 为什么一定要做框架化而不是继续写脚本先说说我为什么坚持要做框架化。最开始我也觉得Agent 嘛不就是调模型、解析输出、执行工具、把结果塞回去再调一次写个 while 循环不就完了。但实际项目里这个 while 循环会迅速膨胀要支持多种工具、要处理模型输出格式不稳定、要记录中间步骤、要控制最大轮次、要处理异常、要做超时保护。等你把这些都加进去那个简单的 while 循环已经变成了一坨没人敢动的代码。框架化的第一个价值是关注点分离。Agent 的运行逻辑循环、状态管理、异常处理和业务逻辑具体做什么、用什么工具、提示词怎么写应该分开。前者是框架的事后者是开发者的事。这样换一个业务场景只需要换提示词和工具框架本身不用动。第二个价值是可测试性。脚本式的 Agent 很难写单元测试因为所有东西都耦合在一起。框架化之后你可以单独测试提示词渲染、单独测试工具执行、单独测试输出解析每个环节都能独立验证。这在调试的时候能省下大量时间。第三个价值是可组合性。真实场景里往往不是一个 Agent 干所有事而是多个 Agent 协作。框架化之后每个 Agent 都是一个标准组件可以像搭积木一样组合起来。这也是现在多 Agent 系统能快速搭建的前提。2.2 SimpleAgent、ReAct、提示词工程三者的关系很多人会把这三个概念混在一起其实它们处在不同的层次上。SimpleAgent是最基础的执行单元它定义了一个 Agent 是什么——有名字、有提示词、有工具集、有模型配置、有最大轮次限制。它不关心具体怎么思考只提供一个容器。ReAct是一种具体的思考范式全称是 Reasoning and Acting核心思想是让模型在每一步先输出思考Thought再输出行动Action执行完得到观察Observation然后进入下一轮。这个范式的好处是把推理过程显式化模型不容易跑偏出问题也容易定位。提示词工程则是把上面两者粘合起来的手段。ReAct 的格式要求、工具的描述、任务的约束全都要通过提示词传达给模型。提示词写得好不好直接决定了 Agent 能不能稳定按预期工作。三者的关系可以这样理解SimpleAgent 是骨架ReAct 是肌肉的运动方式提示词工程是神经信号。缺了任何一个Agent 都跑不起来。2.3 方案选型为什么不用现成的框架市面上已经有不少 Agent 框架了为什么还要自己搭我的理由有三个。第一学习成本 vs 掌控力。现成框架确实上手快但一旦出问题你得去读它的源码才能定位。而 Agent 这类应用对细节极其敏感提示词差一个字、工具描述换一种说法结果可能完全不同。自己搭的框架每一行代码都清楚调试起来心里有底。第二依赖负担。很多框架为了通用性引入了大量依赖一个简单的 Agent 应用可能装了几十个包。自己搭的话核心逻辑可能就几百行依赖只有模型 SDK 和几个基础库。第三定制需求。真实项目里总有一些特殊需求比如特定的日志格式、特定的重试策略、特定的工具调用协议。用现成框架要么改源码要么绕过去都不如自己搭来得直接。当然这不是说现成框架不好。如果只是做个原型验证用现成的完全没问题。但如果是长期维护的项目自己搭一套轻量框架往往更划算。3. 核心模块拆解与实操要点3.1 SimpleAgent 的最小结构设计一个 SimpleAgent 需要哪些字段我经过几次迭代最后定下来这几个核心属性nameAgent 的标识多 Agent 场景下用来区分system_prompt系统提示词定义角色和行为准则tools可用工具列表每个工具有名称、描述、参数 schemamodel_config模型名称、温度、最大 token 等参数max_iterations最大循环轮次防止死循环memory对话历史或中间状态存储这里有个容易踩的坑不要把工具的具体实现塞进 Agent 类里。工具应该是独立的可调用对象Agent 只持有它们的引用。这样工具可以单独测试、单独复用Agent 也不至于变成一个巨型类。另一个坑是max_iterations 的设定。设太小复杂任务跑不完设太大出问题时浪费大量 token。我的经验是默认设 10简单任务 5 就够复杂任务可以到 20。同时要配合超时机制避免单次工具调用卡死。3.2 ReAct 循环的实现细节ReAct 循环看起来简单实现起来有几个关键点。首先是输出格式的约束。模型必须按固定格式输出 Thought、Action、Action Input 三部分否则解析会失败。我的做法是在系统提示词里给出明确的格式示例并且在解析时做容错处理——比如模型偶尔会漏掉某个字段或者用中文写思考而不是Thought解析器要能识别这些变体。其次是观察结果的注入。工具执行完之后要把结果作为 Observation 拼回对话历史让模型在下一轮能看到。这里要注意结果的长度控制如果工具返回的内容特别长比如读了一个大文件直接塞回去会撑爆上下文。我的做法是对结果做截断超过一定长度就只保留头尾中间用省略号代替。第三是终止条件的判断。ReAct 循环什么时候结束有两种情况模型输出了 Final Answer或者达到了 max_iterations。前者是正常结束后者是异常结束。异常结束时应该给用户一个明确的提示而不是静默返回空结果。下面是一个简化的循环伪代码展示核心逻辑def run(self, user_input): self.memory.append({role: user, content: user_input}) for i in range(self.max_iterations): response self.llm.chat(self.memory, toolsself.tools) parsed self.parse_response(response) if parsed.type final_answer: return parsed.content if parsed.type action: observation self.execute_tool(parsed.action, parsed.action_input) self.memory.append({role: assistant, content: response}) self.memory.append({role: user, content: fObservation: {observation}}) return 达到最大轮次限制任务未完成这段代码看起来简单但每一行背后都有讲究。比如parse_response要处理各种格式异常execute_tool要处理工具不存在、参数错误、执行超时等情况。这些细节才是框架能不能用的关键。3.3 提示词工程在框架中的落地方式提示词工程不是写一段好提示词这么简单在框架里它是一套可管理的机制。我的做法是把提示词拆成几个部分角色定义、能力说明、工具描述、输出格式、任务约束。每一部分单独维护运行时拼接。这样做的好处是改一部分不影响其他部分也方便做 A/B 测试。工具描述尤其重要。模型能不能正确调用工具很大程度上取决于工具描述写得好不好。我的经验是每个工具的描述要包含三要素这个工具做什么、什么时候用、参数是什么意思。比如一个搜索工具描述不能只写搜索而要写根据关键词搜索互联网返回相关网页摘要。当需要获取最新信息或验证事实时使用。参数 query 是搜索关键词。还有一个细节是输出格式的示例。与其用文字描述请按 Thought/Action/Action Input 格式输出不如直接给一个完整的示例。模型对示例的模仿能力远强于对规则的理解能力。提示提示词里的示例要覆盖正常情况和边界情况。比如工具调用成功、工具调用失败、需要多步才能完成、直接给出答案这几种情况都给出示例模型的表现会稳定很多。3.4 工具注册与调用的设计工具是 Agent 的手脚设计得好不好直接影响可用性。工具注册我采用的是装饰器方式写起来简洁tool(namesearch, description根据关键词搜索互联网...) def search(query: str) - str: ...装饰器负责把函数包装成标准工具对象提取函数签名生成参数 schema注册到全局工具表。这样新增工具只需要写一个函数加一个装饰器不用改框架代码。工具调用时的参数校验也很关键。模型生成的参数经常有类型错误比如该传字符串传了数字该传列表传了单个值。我的做法是在调用前做一次参数规范化能自动转换的就转换不能转换的返回明确的错误信息让模型重试。还有一个容易被忽略的点是工具执行的超时控制。有些工具比如网络请求可能卡住如果不设超时整个 Agent 就挂在那里了。我的做法是给每个工具设一个默认超时比如 30 秒执行时用线程或异步方式包裹超时就返回错误信息。4. 完整实操流程与关键环节实现4.1 环境准备与依赖选择搭这套框架依赖其实很少。核心就两个模型 SDK 和参数校验库。模型 SDK 看你用哪家参数校验我推荐用 pydantic它能自动从类型注解生成 schema省去手写 JSON Schema 的麻烦。Python 版本建议 3.10 以上因为要用到一些新的类型语法。如果团队里有人还在用 3.8那类型注解得写得保守一点。目录结构我习惯这样组织agent_framework/ core/ agent.py # SimpleAgent 类 loop.py # ReAct 循环 parser.py # 输出解析 tools/ registry.py # 工具注册 builtin.py # 内置工具 prompts/ templates.py # 提示词模板 utils/ logger.py # 日志 errors.py # 异常定义这样分层的好处是每部分职责清晰测试也好写。core 里的东西不依赖 tools 和 prompts 的具体实现只依赖接口。4.2 输出解析器的实现与容错输出解析是 ReAct 循环里最容易出问题的地方。模型输出格式不稳定是常态解析器必须足够健壮。我的解析策略是多级匹配。先用严格的正则匹配标准格式匹配不到就用宽松的匹配比如只找关键词再匹配不到就把整段输出当作 Final Answer 返回。这样即使模型格式跑偏也不至于直接报错。具体来说标准格式是这样的Thought: 我需要先搜索一下 Action: search Action Input: {query: Agent 框架设计}解析器先按行分割找Thought:、Action:、Action Input:开头的行。Action Input 可能是 JSON也可能是纯文本要分别处理。如果 JSON 解析失败就当作字符串处理。还有一种情况是模型直接输出 Final Answer格式是Thought: 我已经知道答案了 Final Answer: 答案是...解析器要能识别Final Answer:这个标记。注意解析器一定要记录解析失败的原始输出方便排查问题。我见过太多次因为解析失败导致 Agent 表现异常结果发现是模型输出了一个没见过的格式变体。4.3 记忆管理与上下文控制Agent 的记忆管理是个技术活。对话历史不能无限增长否则上下文会爆掉成本也会飙升。我的策略是滑动窗口 摘要。保留最近 N 轮完整对话更早的内容做摘要压缩。N 一般设 5 到 10看任务复杂度。摘要用模型生成把之前的思考、行动、观察浓缩成几句话。还有一个技巧是关键信息提取。有些信息在整个任务过程中都很重要比如用户最初的需求、已经确认的事实。这些信息不应该被滑动窗口挤掉要单独存一份关键信息列表每轮都注入到上下文里。上下文长度控制还要考虑工具返回结果。前面提到过长结果要截断。我的做法是设一个阈值比如 2000 字符超过就保留头 1000 和尾 500中间省略。这样既保留了关键信息又控制了长度。4.4 异常处理与重试机制Agent 运行过程中会遇到各种异常模型 API 超时、工具执行失败、输出解析失败、上下文超长。每一种都要有对应的处理策略。模型 API 超时用指数退避重试重试 3 次还失败就返回错误。工具执行失败要把错误信息作为 Observation 返回给模型让模型决定是重试还是换方法。输出解析失败可以尝试重新生成或者用宽松解析兜底。上下文超长要触发摘要压缩。这里有个经验不要让异常直接抛到用户面前。Agent 应该把异常转化成对用户友好的提示同时把详细信息记到日志里。用户看到的是任务执行遇到问题请稍后重试开发者看到的是完整的堆栈和上下文。重试机制要设上限避免无限重试。我的做法是每个环节最多重试 2 次整体任务最多重试 1 次。超过就放弃返回当前状态。4.5 日志与可观测性设计Agent 的调试难度比普通程序高因为它的行为有随机性。完善的日志是排查问题的基础。我的日志分三层DEBUG记录完整的请求响应、INFO记录每轮循环的 Thought/Action/Observation、ERROR记录异常。生产环境开 INFO调试时开 DEBUG。日志格式要结构化方便后续分析。我习惯用 JSON 格式每条日志包含时间戳、Agent 名称、轮次、事件类型、内容。这样可以用工具做聚合分析比如统计平均轮次、工具调用分布、失败率等。还有一个实用技巧是记录 token 消耗。每次模型调用都记录输入输出 token 数累计起来能看到成本。这对优化提示词、控制预算很有帮助。5. 常见问题与排查技巧实录5.1 模型不按格式输出怎么办这是最常见的问题。模型有时候会忘记格式要求直接输出自然语言回答。排查思路先看系统提示词里的格式说明够不够清楚示例够不够多。如果提示词没问题那就是模型本身的问题可以尝试降低温度、换更强的模型、或者在用户消息里再强调一次格式要求。我的经验是格式示例比格式说明有效得多。与其写请按 Thought/Action/Action Input 格式输出不如直接给两三个完整示例。模型对示例的模仿能力很强。如果还是不行可以在解析器里做兜底识别不到标准格式时把整段输出当作 Final Answer。这样至少不会报错只是可能提前结束。5.2 工具调用参数错误怎么处理模型生成的参数经常有问题类型不对、缺字段、多字段、值不合理。处理策略是校验 反馈。调用工具前先校验参数不通过就把错误信息作为 Observation 返回让模型重新生成。错误信息要具体比如参数 query 应该是字符串你传的是数字而不是笼统的参数错误。还有一种情况是模型编造了不存在的工具。这时候要返回工具 xxx 不存在可用工具是...让模型知道可选范围。5.3 Agent 陷入死循环怎么破死循环的表现是 Agent 反复调用同一个工具或者反复输出相似的思考就是不给最终答案。原因通常是任务描述不清楚或者工具返回的结果让模型困惑。排查时先看日志找到循环开始的那一轮看模型当时的 Thought 是什么。解决手段有几个设 max_iterations 硬性截断、在提示词里加如果连续两次得到相同结果请换一种方法或直接给出答案、对重复的工具调用做检测和拦截。5.4 上下文超长导致失败长任务跑到后面上下文会越来越长最终超过模型限制。预防手段是前面说的滑动窗口 摘要。如果已经超了应急方案是截断最早的对话只保留系统提示词、关键信息和最近几轮。还有一个容易忽略的点是工具返回结果的长度。有些工具比如读文件、查数据库返回的内容特别长一次就能把上下文撑爆。这类工具要在实现时就做好截断不要指望框架层兜底。5.5 常见问题速查表问题现象可能原因排查方向解决手段模型不按格式输出提示词不清晰或模型能力不足检查提示词示例增加示例、降低温度、换模型工具参数错误模型理解偏差查看参数校验日志完善工具描述、增加参数示例死循环任务描述模糊或结果困惑定位循环起始轮次设轮次上限、加防重复逻辑上下文超长对话历史或工具结果过长统计各轮 token 数滑动窗口、摘要、结果截断工具执行超时外部依赖慢或卡死查看工具执行日志设超时、异步执行、降级处理结果不稳定温度过高或提示词有歧义多次运行对比降低温度、明确提示词5.6 几个踩过坑才明白的经验第一个经验提示词里的工具描述要和工具实现严格对应。我遇到过工具改了参数名但描述没改结果模型一直按旧参数名调用排查了半天才发现。第二个经验不要迷信大模型。同样的提示词不同模型表现差异很大。选模型要看任务简单任务用小模型又快又便宜复杂任务才上大模型。第三个经验日志要记全。Agent 出问题时你永远不知道是哪一轮、哪个环节出的问题。完整的日志能省下大量排查时间。第四个经验先跑通再优化。不要一开始就追求完美的框架设计先让最简单的版本跑起来再根据实际问题迭代。我见过太多人卡在设计阶段最后什么都没做出来。6. 框架的扩展方向与个人实践体会框架搭好之后能扩展的方向其实很多。比如加多 Agent 协作让不同角色的 Agent 互相调用加工具的动态加载运行时从配置里注册工具加执行轨迹的可视化把每轮的思考行动画成流程图方便调试和演示。我自己在实际项目里最常用的是执行轨迹记录。每次 Agent 运行完把完整的 Thought/Action/Observation 序列存下来出问题时回放一遍比看日志直观得多。这个功能实现起来不难但对调试效率的提升非常明显。还有一个我觉得值得投入的方向是提示词的版本管理。提示词改动对 Agent 行为影响很大但很多人改提示词就是直接改代码改完也不记录。我的做法是把提示词单独存成文件每次改动都记版本配合 A/B 测试看效果。这样能积累出针对特定任务的优质提示词库。最后分享一个小技巧给 Agent 加一个思考预算。在提示词里告诉模型你最多有 5 步可以完成任务模型会更倾向于高效行动而不是无谓地反复思考。这个技巧在简单任务上效果特别明显能把平均轮次降下来不少。这套框架我用了大半年从最初的几百行迭代到现在核心逻辑其实没怎么变变的是各种边界情况的处理。Agent 开发就是这样跑通容易跑稳难。框架化的价值就在于把跑稳这件事变成可复用、可积累的东西而不是每个项目都重新踩一遍坑。
返回列表