
1. 项目缘起为什么需要 Agent-ReachAI Agent 的圈子最近一年到一年半的时间变化太快。从最早的单一技能助手到后来能调用工具的复合体再到行业内讨论的“其实 Agent 大多数情况下还是在靠大模型死磕工具也没用得多聪明”。我自己的很多项目也卡在这个瓶颈上模型本身能力没问题但一放到真实业务场景里就露怯——不是因为模型不会说而是因为它“够不着”能帮它做事的那些东西。“Agent-Reach”这个项目名老实说来源于一次很直接的挫败感。当时我在做企业知识库问答机器人用户抛给Agent的问题稍带点复杂它就容易跑偏比如问“我们公司最近三个月的售后满意度变化趋势是什么”Agent 拿到问题后只知道翻知识库里的文档如果文档里没有现成的图表或结论它就开始兜圈子绕半天然后生成一段模棱两可的废话。后来我才意识到问题不在 Prompt 上头而在于 Agent 的能力边界太窄——它没有手也没有眼睛更别说主动去调数据库算趋势了。所以我给这个项目取名 Agent-Reach想法很朴素把 Agent 的“触角”尽可能伸长让它不仅能查文档还能查接口、查数据库、操作文件、自己写临时脚本甚至在特定场景下把任务拆给另外一个更擅长干这类活的子 Agent。简单说就是把聪明的大脑接上一副能干活的手脚。我立项时定下的目标不是做出“看起来聪明”的 Demo而是让 Agent 在一个真实工作流里连续完成多步任务中途尽量不让人工干预并且每一步我都知道它在干什么、为什么这么干。这篇文章会把整个项目从思路到代码再到踩坑过程完整拉一通。内容适合两类朋友一类是刚接触 Agent 开发、想找一套能落地的工程参考的另一类是已经做过简单 Agent 但觉得能力始终不够、想知道怎么拆解和扩展架构的。我会把设计决策背后的原因都讲清楚不只是贴配置和代码。2. 整体设计拆解从“会聊天”到“能干活”2.1 Agent 的核心困境只会“想”不会“做”传统的大语言模型应用说白了就一个套路用户提问 → 模型回答。大多数失败的 Agent 案例都倒在同一个地方——模型把回答任务和行动任务混在一起了。你问它“帮我统计一下”它真的用一顿文字推理代替了实际去算。不是说大模型不会推理而是它根本没有数据来源和操作入口只能靠训练时见过的“知识”去猜而知识库里沉淀的往往不是最新、最准确的数据。我把这个困境拆成了三个具体问题知识滞后模型训练有截止日期业务数据永远在增长光靠 Prompt 喂不进去那么多动态信息。工具割裂企业里大量能力封装在 API、数据库、脚本、内部系统中Agent 如果不能调用这些就只能游离在业务系统之外。任务无感Agent 对自己的“执行活动”没有感知它不知道工具返回了什么、该怎样基于返回值调整下一步所以遇到预期之外的情况就很容易原地打转。Agent-Reach 的思路就是不试图把这些问题全部丢给大模型解决而是构建一套“感知-决策-行动-反馈”的外挂系统。大模型只负责最擅长的推理部分其余的都交给工程机制去补。2.2 核心设计ReAct 循环 工具注册表我在设计阶段对比过很多方案纯粹的 Function Calling、LangChain 式的链式调用、微软的 Semantic Kernel、自研的 ReAct 循环。最后选定的是一条偏工程化的路线——ReAct Reasoning Acting 循环外加一个统一的工具注册表。原因有三ReAct 循环对模型能力的要求是可控的。它本质上就是不断地“推理 → 决定调用什么工具 → 观察结果 → 再推理”的循环不需要模型一次性生成整个计划再执行后者对长任务的稳定性非常不友好。工具注册表可以让我很方便地控制“Agent 能看到什么”。不需要改动核心逻辑只要改注册表配置就能给 Agent 增加或屏蔽一项能力。任何工具本质上都是“输入 JSON → 输出 JSON”这让我在调试的时候有统一的监控日志格式排查问题一目了然。其实我很早就想明白了一件事决定 Agent 上限的往往不是大模型而是你让它能碰到的工具集合。模型再聪明伸手够不到东西也没用。所以整个项目里工具注册表和执行环境的稳定性优先级最高甚至排在了 Prompt 工程前面。3. 核心细节解析工具注册表与任务编排3.1 工具注册表的结构设计工具注册表是整个 Agent-Reach 的中枢神经。每个工具都遵循同一套接口规范用 JSON Schema 描述入参和返回值让大模型可以“阅读”工具说明并按约定调用。我用一个 dataclass 来定义工具的基本结构dataclass class ToolSpec: name: str # 工具的全局唯一名称 description: str # 给LLM看的自然语言描述越具体越好 input_schema: dict # JSON Schema声明入参结构和必填项 handler: Callable # 真正的执行函数接收dict入参 timeout: float 30.0 # 单次调用超时防止Agent挂起 retry_on_error: bool False这里有一个我踩过不少坑后总结的细节工具的 description 不能偷懒。很多人写两句话就完事结果大模型经常选错工具或填错参数。我现在要求每个工具的 description 必须包含三块内容这个工具负责干什么、什么业务场景下该用、哪些常见误解需要避开。比如一个查询售后工单数量的工具描述不应该只是“查询工单”而应该是用于查询指定时间范围内的售后工单数量。 当用户询问关于维修、退货、投诉、质量问题等售后场景的数量统计时应优先调用此工具。 注意该工具返回的是汇总数量明细查询请使用query_ticket_detail工具。你可能会觉得这有点啰嗦但在实测中这样的描述能把工具误选率从接近 20% 压到 5% 以下代价只是多花几十个 token非常划算。3.2 任务编排让 Agent 学会“分步走”ReAct 循环本身不复杂难在怎么把“一步”定义好。我在实现时参考了斯坦福那个经典的 ReAct 论文但做了几个工程化的调整首先给模型一个步骤上限。官方论文里没有强调这个但工程上如果 Agent 陷入死循环没有上限会导致白白烧钱、拖垮链路。我默认上限是 6 步超过后让 Agent 汇总当前已获得的信息并给出阶段性结论而不是无限继续。其次要求模型在每一步显式输出thought、action和action_input。Model 输出这些字段我是不直接执行其“自然语言计划”的只有 action 字段匹配到某个注册过的工具名时才真正执行。这相当于在模型输出与外部执行之间加了防火墙避免出现模型“嘴上说要调工具实际却没按格式来”的混乱局面。最后是状态收集。每一轮工具调用的输入和输出我都会追加到对话历史里但不是原样追加而是做一次轻量化的压缩。比如当工具返回一段很长的表格时我会截断到五行并且加一句“数据已截断如需完整内容请调用详细查询工具”。这一步能很好地控制上下文长度避免 3~4 轮之后上下文膨胀导致模型开始胡言乱语。3.3 子 Agent 的触发机制Agent-Reach 还有一个让我觉得比较骄傲的设计当主 Agent 发现自己解决不了某个专项问题时可以主动“摇人”。这个机制不是复杂的多智能体协商而是一个简化版的委派模式。我在工具注册表里放了一个特殊工具叫delegate_to_sub_agent它的入参包括子 Agent 的技能标签和任务描述。当主 Agent 认为自己检索了大量资料但无法整理出结论时它会调用这个工具把任务丢给比如“数据分析Agent”“文档检索Agent”等专项子 Agent。子 Agent 跑完后再把结果返回给主 Agent由主 Agent 统一组织最终答案。这听起来容易但实践中最难的是防止两个 Agent 互相踢皮球。我的解决办法是给子 Agent 设定硬边界子 Agent 只能处理接收到的那一个任务不得发起新的委派、不得调用非本技能域内的工具。从设计上切断递归级联换来的是系统的可预期性。4. 实操过程从零搭起一套最小闭环4.1 第一步搭一个最小的 ReAct 循环不依赖 LangChain我用 Python OpenAI SDK 直接实现了最小的 ReAct 循环。先定义模型的“决策输出格式”利用模型对 JSON 的天然支持让它在每轮返回一个结构化的 JSON{ thought: 用户想统计三个月的售后满意度趋势需要先查询工单数据, action: query_ticket_trend, action_input: {start_date: 2025-01-01, end_date: 2025-03-31} }核心循环拿 Python 写出来大概是下面这种感觉def run_agent(query: str, tool_registry: ToolRegistry, max_steps: int 6): messages [{role: system, content: SYSTEM_PROMPT}, {role: user, content: query}] for step in range(max_steps): response llm.chat(messages, toolstool_registry.schema) msg response[choices][0][message] if not msg.get(tool_calls): return msg[content] # 模型认为不需要再调用工具直接返回最终答案 messages.append(msg) for call in msg[tool_calls]: tool tool_registry.get(call.function.name) result tool.execute(json.loads(call.function.arguments)) # 把工具运行结果追加回对话 messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse) }) return 已达最大步骤上限 summarize(messages)这个循环看起来简单但有一个关键的工程细节必须让模型感知到每一步的工具返回结果。如果你只是把工具结果存在变量里、却塞不进对话上下文模型下一轮就变成“盲人摸象”根本不会根据返回结果调整后续动作。我之前犯过这个错误——整整调了一天才发现是消息追加逻辑漏了tool_call_id导致结果和调用没有正确关联。4.2 第二步实现一个实用的内置工具有了循环体要靠工具来填充它的能力。下面是我实现的一个查询 MySQL 数据库的工具它在项目里承担了所有结构化数据的获取和计算def query_database(sql: str) - dict: 执行只读SQL查询返回结构化结果。 禁止在该工具中执行任何写操作包括INSERT/UPDATE/DELETE/DDL。 if not sql.strip().lower().startswith(select): return {error: 只允许SELECT语句} with mysql.connector.connect(**DB_CONFIG) as conn: cursor conn.cursor(dictionaryTrue) cursor.execute(sql) rows cursor.fetchmany(50) # 限制一次最多取50行 return {rows: rows, row_count: len(rows)}这里我坚持了两条原则。一是只读约束Agent 调用数据库的权限必须是严格只读的。Agent 是概率系统你无法保证它百分之百不犯错如果放开写权限一条错误的 DELETE 语句可能毁掉整个业务表。二是一次性行数限制防止 Agent 生成一条不带 WHERE 的查询把全表几百万行拉进上下文既撑爆 token 也拖垮数据库。很多做 Agent 的朋友一上来就追求工具多、功能全但我把所有工具列出来后砍掉了大概三分之一只保留真正产生业务价值的。一个搜索功能、一个数据库查询、一个文件读写、一个子 Agent 委派、一个网页抓取加上这几个基本能覆盖大多数真实问题。工具贵精不贵多注册表里的工具越多模型在每一步做工具的“意图识别”就越困难准确性反而下降。4.3 第三步给 Agent 设计“观察到失败”的反馈机制真实世界里工具调用不会总成功。可能是数据库超时、可能是 API 限流、可能是查询结果为空。Agent 能不能从失败里恢复决定它在生产环境里能不能用。我特意在工具层设计了统一的三类返回状态success正常拿到结果。partial_success拿到了一部分数据但存在截断、超时或缺失需要 Agent 决定是否重试或换方案。failure执行失败返回错误信息和可能的原因。然后在系统 Prompt 里明确要求 Agent当你看到 failure 时必须先说明失败原因再提出至少一种备选方案而不是直接把这个错误丢给用户当作最终答案。就像是给 Agent 装了一个默认的“预案机制”即使它不能完成任务也必须给出已尝试路径的分析和替代选项。这个设计看起来很简单但它把 Agent 的行为从“一条路走到黑”改成了“动态尝试 透明汇报”。我在排查问题时受益特别大因为每一步都有状态码出错了能立刻定位到是工具的问题、模型的选择问题还是数据本身的问题。5. 部署与性能从 Demo 到稳定的关键一步5.1 如何配置 LLM 和参数整个项目跑起来后我花了不少时间在调对话参数上。这里直接给出我实测后觉得比较稳的一套:ModelGPT-4o 级别及以上低于这个档位的模型在复杂工具选择上会明显吃力。temperature0.2防止模型发散。Agent 任务和聊天不一样你需要的是确定性不是文采。max_tokens按最大任务复杂度配置但一般在 2000 左右如果任务太重可以把中间推理步骤拆成多步不要指望一步输出超长结果。streaming关闭。虽然流式输出对用户观感好但在 ReAct 循环里我一般要完整拿到 JSON 再解析流式反而增加复杂性。这批参数是我在几轮压测之后定下来的。特别是 temperature一开始图新鲜设成 0.7Agent 在工具选择上就跟喝了酒一样一会儿选查询工具一会儿选文档工具让我非常头疼调低到 0.2 之后气质立刻稳定了。但这个参数跟模型和场景有很大关系如果你的 Agent 做的是开放性创意任务那 0.2 可能太呆板需要酌情提高。5.2 链路性能的监控和缓存Agent-Reach 的每轮任务都是一连串的模型调用与工具调用一次完整任务跑下来可能需要 10~20 秒。用户等得久链路里任何一个环节出错都会放大成整体的失败。所以我做了两件事来提升稳定性第一全链路日志。每个步骤的输出我都记录到结构化日志里字段包括步骤序号、输入消息摘要、模型选择的 action、工具执行耗时、工具返回状态码、累计 token 消耗。排错时直接看这条日志链能非常快地定位到是哪一步出的岔子。第二缓存机制。很多用户的提问是重复的或者只是在时间维度上略有变化。我对“完全相同”的请求做了一层 10 分钟的本地缓存见缓存直接返回上一次完整答案而对于数据库查询工具我根据 SQL 语句哈希做了 60 秒的缓存。实测下来 API 调用费用降低了大约 25%而且用户体验没有明显下降。不过强烈提醒一句任何有副作用工具的响应都不能缓存这是工程红线。5.3 安全与权限控制清单Agent 一旦接入了数据库查询和文件操作安全问题就避不开了。我整理了一些实际落地的控制措施数据库账号隔离单独建一个最小权限账号只给特定库表的 SELECT 权限绝不使用 root 或业务主账号。SQL 防火墙除了白名单校验 SELECT 开头之外我还会用正则过滤掉注释符、多语句拼接等危险特征不要让模型有机会构造出奇怪语句。文件操作隔离Agent 能读写的目录固定在一个沙箱路径内禁止通过../跳出也不允许访问工作目录之外的文件。敏感信息脱敏工具返回的内容如果包含手机号、身份证、密码之类的字段在返回给模型之前就做脱敏处理避免敏感数据进入模型上下文。6. 常见问题与排查技巧实录6.1 问题一Agent 反复调用同一个工具不往前走这是我在项目初期遇到最频繁的问题。比如让它查工单趋势它查完一次之后不分析结果反而又生成一条一模一样的 SQL 再去查反复三五次后直接触发步骤上限。排查后我发现根因不在模型而在 Prompt 里缺少“何时停止”的引导指令。模型认为只要目标任务没有最终回答就应该继续调用工具。于是我在系统 Prompt 里加了一句如果你已经获得了回答用户问题所需的全部信息请停止调用工具直接基于现有信息组织最终答案。这句话加上去之后无效工具调用率下降了非常明显从平均 3.5 次降到了 1.2 次左右。还有一种情况是工具返回结果为空但用户问题确实需要数据支撑这时 Agent 会因为得不到信息而反复重试。针对这种场景我加了“空结果判定”的逻辑如果某工具的连续两次返回结果为空且查询条件相同第三次直接触发失败保护要求 Agent 明确告知用户“当前条件下查询不到相关数据”而不是继续空转。6.2 问题二上下文爆炸模型开始“失忆”看起来这是个 token 问题但实际是消息管理问题。Agent 每做一次工具调用都会把工具返回内容拼接进上下文最多 6 步任务积累下来上下文里可能充斥着几百行的 JSON 表格。我做了一个叫ContextCompactor的模块专门负责压缩旧的工具返回消息def compact_messages(messages, max_tool_result_chars800): result [] for msg in messages: if msg[role] tool and len(msg[content]) max_tool_result_chars: # 保留前200字 中间摘要 后100字其余省略 shortened msg[content][:200] ...[内容已压缩]... msg[content][-100:] msg[content] shortened result.append(msg) return result这个压缩逻辑简洁但非常有效。每次工具返回对象是超长列表的时候我会在压缩的同时附上“如果用户需要完整数据可以提示用户调用导出的工具获取明细文件”。把细节放在外部、把结论留在上下文中这样模型既保留了“知道结果”的能力又不会因为细节过载而分心。6.3 问题三工具描述互相模糊选错工具有时候 Agent 明明该查售后工单的却跑去调了商品库存查询工具。这种问题在工具数量超过 5 个之后开始频繁出现原因是工具之间的 description 写得太像了模型根本区分不了。解决方法是给每个工具加了使用场景和反例。比如商品库存工具的描述里明确说明“如果用户问的是售后问题不要使用本工具应该使用 query_ticket”。这相当于在工具描述层面做了一道“路由排除”模型在对比之后更容易选出正确项。我测试下来6 个工具场景下的选择准确率从 91% 提升到了 97%提升幅度非常可观。6.4 问题四外部 API 超时导致整个任务崩溃工具调用是同步阻塞的如果某个 API 需要 10 秒才返回整个 Agent 任务就得等 10 秒。更糟的是如果这个 API 一直不返回任务就挂死在那里。我的方案是为每个工具设置独立的超时时间并且给慢工具设计了两种降级策略。一是改为异步轮询第一次调用只提交请求拿到任务 ID立即返回给 AgentAgent 再用一个check_task_status工具轮询状态直到结果就绪。二是快速失败超过 5 秒没返回就直接返回失败状态和原因Agent 换个工具或者请示用户而不是傻等。这两个策略不冲突有时可以叠加使用。实际应用中我把大多数查询类工具的超时都定在 8~10 秒而异步轮询模式主要用在例如生成报表、跑数据任务这类重量级操作上。7. 实测效果与复盘数据说话项目上线后我在两个典型场景上做了对比测试。一个场景是“查询售后满意度趋势”属于跨表数据统计另一个是“根据知识库文档总结产品使用注意事项”属于文档检索与归纳。测试结果场景未用工具时的准确率使用 Agent-Reach 后平均完成步数平均耗时售后满意度趋势32%88%3.8 步9.6s文档归纳总结76%92%2.5 步7.2s数字最能说明问题没有工具的 Agent 处理数据问题时基本就是靠猜准确率低得吓人接入工具后准确率大幅攀升同时因为模型的每一步都有工具结果作为事实依据最终答案的可靠性也明显提高。在文档归纳这个偏“文本活”的场景里工具的增益相对小一些但仍然带来了显著的提升——因为工具可以让 Agent 先搜索再总结而不是靠训练记忆硬写。我的复盘结论是Agent-Reach 的价值不在于它用了多么高深的技术而在于它把一个朴素的理念落地得很扎实——不要逼大模型去当万事通给它配一套能干活、好监控、有边界的工具链。大模型负责聪明的部分推理、规划、表达工程系统负责确定的部分调用、执行、校验两者各司其职整体系统才真正可用。8. 如果要继续扩展我会做什么Agent-Reach 目前已经能解决“Agent 没有手脚”的问题但我知道它离“好用”还有很长的路要走。按我自己的规划接下来会优先做四件事第一是接入 Memory 模块。当前 Agent 是无状态的也就是说它在这一轮积累的查询经验、数据偏好下一轮完全丢失。加入短期记忆和长期记忆机制后Agent 可以在后续任务里直接复用之前的高频查询模式和数据结论减少空转和重复劳动。第二是加入多轮任务规划能力。目前的 ReAct 循环是贪心的、逐轮走一步看一步的对复杂任务缺少一个全局路线图。后续想引入任务规划器在任务开始时先生成一份粗粒度计划然后在执行中根据实际情况动态修正让复杂任务的推进更有条理。第三是扩展到更多数据源。数据库、文档、搜索是标配了但真实业务数据往往散布在 SaaS 系统、内部表格、邮件、即时通讯记录里。每接入一个新数据源Agent 的可用范围就扩大一圈。这个方向没有技术难度但要求极强的领域理解能力是慢工出细活的部分。第四是完善用户交互的确认流程。凡是涉及“可能对数据产生外部影响”的调用目前我已经做了拦截和只读保护但更理想的方式是前端弹一个可交互的确认面板让用户看清楚 Agent 即将执行什么再放行。尤其是在商业环境中透明和可控比智能和流畅更容易获得使用者的信任。我在实际做 Agent-Reach 这个项目的过程中最深的一个感受是做 Agent 系统真正的难点从来不是模型选型也不是 Prompt 技巧而是如何把一个概率系统安放在需要确定性的工程环境里并且让它仍然有用、可信、可控。这需要你在架构设计、工具封装、错误处理、监控告警上投入大量看似“不那么酷”的功夫但最终撑起产品价值的恰恰就是这些笨功夫。如果各位朋友也在做类似的 Agent 项目我最想给的一条建议是先别急着堆功能把你现有的两三个核心工具打磨到极致把日志和反馈机制做到位再慢慢加触角。Agent 的扩展能力建立在稳定内核之上地基歪了伸出去的每一根触角都容易把整座楼带塌。