ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent-Reach:给大模型安装能操作系统的工具之手

Agent-Reach:给大模型安装能操作系统的工具之手

1. 项目定位:Agent-Reach 解决什么问题

1.1 名词拆解:Reach 到底指什么

先把这个名字拆开看。"Agent" 最近一年几乎被说烂了,但"Reach"这个词值得琢磨。我在做这个项目之前,翻来覆去想了很久,最后确定下来:这里说的不是"代理服务器"里的 agent,也不是"触达用户"里的触达,而是让一个大模型驱动的智能体,真正把能力伸到业务现场——也就是"够得着"实际系统的那一步。

什么叫做"够得着"?举个例子。你用 ChatGPT 聊天,它能帮你写一段 Python 代码,但如果你让它"读一下服务器上 /data/logs 目录下最新的错误日志,整理成一份报告发到群里",它就抓瞎了。不是模型不够聪明,而是它根本没有手去操作文件系统、没有权限去调接口、没有通道去发消息。Agent-Reach 要解决的,就是给大模型装上一只"够得着"的手。

我见过太多号称"智能体平台"的项目,演示的时候很漂亮,一问"能接我们内部系统吗"就卡住了。要么工具注册流程重到让人绝望,要么只支持某一家云厂商的模型,要么跑两步就上下文爆炸。这类东西不是给开发人员用的,是给 PPT 用的。我在做 Agent-Reach 之前定了几条硬规矩:

  • 模型无关,国产开源模型和商业 API 都能接
  • 工具接入必须简单到一个文件搞定
  • 整个链路可观测,出了问题能看到是哪一步崩的
  • 不依赖重型基础设施,一台普通机器就能跑

这个项目适合谁?适合那些已经在用大模型 API 做单点能力,但想更进一步把模型接入实际业务流程的人。不管你是后端开发、算法工程师,还是运维想搞个自动化助手,这篇文章里讲的思路和代码,都可以直接改吧改吧拿去用。

1.2 目标不是"又一个框架",而是跑通全链路

说实话,LangChain、AutoGen 这些框架已经很多了,Agent 相关的开源项目更是扎堆。Agent-Reach 如果只是一个"封装好的调用工具",那没有存在的必要。我做这个项目的核心出发点,是把一条链路完整跑通:

任务进来 → 模型理解 → 拆解步骤 → 挑选工具 → 执行操作 → 观察结果 → 决定下一步 → 输出最终结论

这条链路里每一步都有坑。模型可能选错工具,工具可能返回超时,参数可能对不上,执行可能产生副作用。Agent-Reach 的核心不是某一步做得多么惊艳,而是把整条链路整合得足够稳,并且每一步都留下日志和审计线索。

现状是大部分团队还在"单轮调用"阶段:写个 prompt,调一次 API,拿结果。稍微往前走半步,就发现模型返回不稳定、工具多了以后容易选错、循环执行容易失控。Agent-Reach 的定位不是一个研究性质的 demo,而是一个"生产环境能跑"的工具箱。后面我会把每一层的设计思路和代码都铺开讲清楚。

2. 整体设计思路与模块划分

2.1 为什么我不直接选现成的编排框架

在动工之前,我实测过市面上几个主流框架,说实话各有优势,但我最终还是决定自己写一个轻量实现。原因有几点,比较现实:

第一,所谓"编排框架"其实解决得最多的场景是从零开始把 agent 搭起来,但对于要把 agent 嵌进自家业务的团队来说,最大的成本在于工具适配和数据流转,而不是"怎么把模型聊起来"。框架太重,反而碍事。

第二,很多框架的执行逻辑是黑盒。模型内部在干什么、每一步选择了什么工具、为什么这么选,缺少清晰可查的日志。我调过一段时间之后发现,没有可观测性的 agent 根本没法维护。生产环境里出一次错,你想定位是模型指令理解错了、工具入参格式错了、还是执行环境缺权限,如果日志不完整,光靠猜就能折腾一整天。

第三,大多数框架对工具的描述格式要求特别死板。好一点的用 JSON Schema,差点的是自己发明一套 DSL。如果团队里有多个业务方都要接入工具,学习成本和踩坑成本就被拉高了。

所以 Agent-Reach 的设计原则就三条:薄封装、全日志、工具定义尽量用标准格式。我宁可多写几个适配器,也不愿意把所有东西都捆在一个框架里。

2.2 四个模块,各管一段

整个项目分成四个模块,每一层都有独立职责:

  • 模型接入层:统一封装不同大模型的调用方式。不管你是接 OpenAI 兼容接口、通义千问、DeepSeek,还是本地部署的 Qwen、GLM,都可以通过一套接口切换。底层的响应格式差异、超时处理、错误重试,都在这一层磨平。
  • 工具注册层:这是 Agent 能不能"够得着"业务的关键。每个工具本质上就是一个函数,带一段描述和一个参数 Schema。Agent 根据任务需求从注册表里挑工具,就像人翻通讯录找联系人一样。
  • 决策执行层:负责跑"思考-行动-观察"循环。模型先想下一步该做什么,然后选择工具、生成参数、执行调用,拿到结果后再判断任务是否完成。这一层需要精细设计,不然容易陷入死循环或花冤枉钱。
  • 观测审计层:每轮循环的输入输出、工具调用、耗时、Token 消耗,全部落日志。这个层我一开始觉得无所谓,后来做过几个复杂任务后才发现,没有它你根本没法排查问题。

这四个模块合起来,就是一个"能用的大模型执行终端"。换个角度理解,它像一个接线板:大模型是插座,工具是电器,Agent-Reach 负责把插头对准插孔,并且盯着电流是不是正常流动。

3. 模型接入层的核心细节

3.1 统一接口解决了什么痛点

市面上的大模型 API 形态五花八门。有的只支持消息补全,有的提供流式输出,有的函数调用格式不一样,有的返回字段叫content,有的叫text。如果业务代码直接调各家 SDK,后期换模型等于重写一遍调用逻辑。

Agent-Reach 的模型接入层做了一个统一的LLMClient接口,核心只暴露两个方法:

class LLMClient(ABC): @abstractmethod def chat(self, messages: list[dict], tools: list[dict] | None = None, **kwargs) -> LLMResult: """发送消息,可选附带工具列表""" pass @abstractmethod def parse_tool_call(self, response: LLMResult) -> ToolCall | None: """从模型响应中解析出工具调用请求""" pass

调用方不关心底层用的是哪一家模型,只需要拿着消息列表和工具描述发给chat,然后从返回结果里取文本或工具调用。各家模型的差异都写在适配器里,新接一个模型就写一个适配器,改一行配置就能切换。这个设计给我后续接入更多模型省了很多时间。

有个关键点要提醒一下:不同模型的工具调用格式未必一致,哪怕都声称支持 OpenAI 兼容格式。有些模型内部做了一层转换,传进去的 tool 描述和模型实际认为的格式会有一点偏差,返回的字段也可能存在细微差异。千万不要想当然以为兼容格式就等于完全一样,每个模型换上来之后,一定要先跑几个带工具调用的测试用例,把返回结果打出来肉眼观察一遍。

3.2 流式输出要不要支持

我在第一版里直接忽略了流式输出,只做一次性非流式调用。原因是 Agent 执行任务时,通常需要拿到完整的结构化结果再决策,流式输出对体验帮助不大,反而把代码复杂度搞上去了。但如果你的场景要求"边生成边展示",就必须考虑流式。我在 Agent-Reach 里预留了stream=True的开关,但决策循环默认不开启。

这里给你一个参数选择的参考:

场景流式非流式
用户等待最终结果可用推荐
agent 多轮工具调用不推荐推荐
对话式生成长文推荐可选
后台批量任务不需要推荐

就我个人的经验来看,Agent 编排链路里流量消耗大头在"思考"的过程,这部分用户并不关心快慢,反而更在意结果是否正确。先用非流式把逻辑跑稳,后面真有展示需求再加流式,是比较务实的路线。

3.3 超时与重试机制

模型接口不稳定是常态,尤其是自部署的模型,偶尔一个请求能卡两三分钟。在 Agent 循环里,一次工具调用的等待时间会被放大,因为后面还挂着好几轮循环。

我的做法是给chat设置两个层次的时间控制:

  • 连接超时 10 秒
  • 读取超时 120 秒(模型小的话可以缩到 60 秒)

重试逻辑采用指数退避:第一次重试等 2 秒,第二次 4 秒,第三次 8 秒,最多重试 3 次。如果再失败就把异常抛给决策循环,让 Agent 自己判断是换模型、换工具,还是直接放弃。不要试图在底层无限重试,那会把问题掩盖掉。

4. 工具注册层:让 Agent 能操作真实系统

4.1 工具的本质就是一个函数加一段说明书

工具层是整个 Agent-Reach 里最有实际价值的部分。我理解的工具,本质上就是"能被大模型调用的函数"。函数什么人写的?我们自己。大模型怎么知道该调哪个?靠我们提供的"说明书"。

这个说明书要用模型能看懂的方式写清楚几件事:

  • 这个工具是干什么的
  • 入参有哪些,每个参数的类型和含义
  • 有没有副作用,比如"会删除数据"、"会发邮件"这种要特别标注
  • 返回值大概长什么样

我把工具定义设计成一个 Python 类,注册的时候自动从类型注解和 docstring 生成描述,不需要单独维护一份 JSON。这样做的好处是业务代码里改一下参数说明,模型拿到的工具描述也会同步更新,不会出现"代码改了,配置忘了改"的问题。

示例定义:

@register_tool def search_logs(keyword: str, hours: int = 24) -> list[dict]: """ 在日志目录中搜索包含指定关键词的日志条目。 Args: keyword: 要搜索的关键词,必填。 hours: 搜索最近多少小时的日志,默认 24 小时。 Returns: 匹配的日志列表,每条包含时间戳、级别、消息。 """ ...

注册器会根据函数的__doc__和类型注解,自动生成结构化的工具描述。模型看到的就是一份适合它理解能力的工具目录。

4.2 工具描述写得越具体,模型越不容易选错

踩过这么多坑之后,我最想跟你强调的就是:工具选错,绝大多数时候不是因为模型笨,而是因为你没把工具描述写清楚。

举个例子。我一开始给"查天气"工具写的描述是"获取天气信息",结果模型在用户要求"明天要不要带伞"时,居然去调用了一个"发邮件"的工具——因为它觉得"提醒带伞"和"发邮件提醒"有关系。问题就出在描述太模糊。

后来我把描述改成"获取指定城市当天或未来几天的天气情况,包括降水概率、温度、风力。当用户询问出行、穿衣、带伞建议时,优先调用本工具。"准确率立刻上去了。这个原则适用于所有工具:把触发条件写进描述里,把边界情况也写进去。

另外,工具的参数说明也很关键。模型会根据参数名和描述来填充值。如果参数叫date,但你期望的是YYYY-MM-DD格式,必须在描述里写清楚。不然模型可能会给你传一个"明天"或者"2024-06-01 00:00:00"之类的东西。

4.3 工具权限与副作用控制

不是所有工具都适合让模型无条件调用的。如果一个工具能删除数据、能转账、能对外发送消息,那在没有任何限制的情况下让模型自由调用就是在给自己埋雷。

我在 Agent-Reach 里给每个工具标记了risk_level:

  • L1 只读:查日志、查配置、搜索资料,模型可直接调用
  • L2 有副作用:写缓存、更新状态、发通知,需要二次确认
  • L3 高危险:删数据、改权限、大规模变更,默认禁止模型直接调用,需要人工审核

这个设计非常有用。实际跑起来你会发现,模型的自主性越强,越容易出现"好心办坏事"的情况。比如我让 Agent 帮我清理临时文件,它理解成"删除 /tmp 下所有文件",如果没有权限控制,那这波操作就很酸爽了。

5. 决策执行层:循环是怎么跑起来的

5.1 核心循环:思考、行动、观察

Agent 的决策循环并不神秘,一句话就能说明白:让模型看完当前状态,决定下一步动作,执行完看结果,再决定下一步,直到它认为任务完成了。

用伪代码表达就是:

while not done and step < max_steps: result = llm.chat(messages, tools) if result.tool_call: # 执行工具,把结果追加到消息里 messages.append(tool_result) else: # 模型没有调用工具,视为给出最终回复 done = True

这个循环看着简单,真正要跑稳有几个细节。第一个是max_steps。如果任务一直完不成,模型会反复调用工具,没完没了。我的默认值是 8 步,复杂任务可以调到 15,但超过 15 步基本就该怀疑是 prompt 设计有问题了。

第二个是把历史消息全部留给模型看。工具调用的输入、输出都要拼接到上下文中。这会带来一个问题:Token 消耗会逐渐变大。你需要做上下文裁剪,把太早的、不太相关的工具输出压缩掉。我通常保留最近 3-5 轮的完整对话,更早的操作结果只保留一行摘要。

5.2 如何处理模型返回不稳定

模型返回的工具调用参数,不是每次都能直接拿来用。最常见的问题是缺参数、多传参数、类型不对。比如工具需要user_id: int,模型给你传了一个字符串"abc123",或者根本漏了这个字段。

我的处理方式是在执行工具前,加一层参数校验和类型转换。用 Pydantic 或类似工具做 Schema 校验,不合格的参数尝试转型,实在不行的就把它当错误信息反馈给模型,让它重新生成参数。要让模型知道"刚才那次调用因为参数问题失败了,请重新给一份正确的参数",往往模型就能纠正过来。

这个"反馈错误让模型自己纠错"的机制非常强大,比你硬写一堆兼容代码要省事得多。模型的自我纠错能力比很多人想象的要强,只是你需要给它足够的反馈信息。

5.3 并行调度怎么实现

有些任务天然可以拆成多个独立子任务。比如"查一下三个城市明天的天气,汇总给我",就没必要一个城市一个城市串行调用。Agent-Reach 支持一个 step 内触发多个工具调用——模型可以一次返回两个或三个独立调用,然后我用一个线程池并行执行。

from concurrent.futures import ThreadPoolExecutor def execute_tools(calls: list[ToolCall]) -> list[ToolResult]: with ThreadPoolExecutor(max_workers=min(4, len(calls))) as executor: futures = [executor.submit(execute_tool, call) for call in calls] return [f.result() for f in futures]

并行虽然快,但也有坑。一是模型把两个有依赖关系的工具调用同时发出来,比如它先调"获取用户列表"再调"给每个用户发通知",如果并行执行,后面那个可能拿不到前面那个的结果。所以我做了个简单判断:如果这些调用之间存在明显的参数依赖关系,就强制串行。这层判断逻辑不复杂,但能避免很多诡异的 bug。

6. 实操环节:从零搭一个 Agent-Reach 实例

6.1 环境准备与目录结构

Agent-Reach 的运行环境要求不高。我本地开发用一台 8 核 16G 的机器,模型接的是云端 API,本地只跑决策和执行逻辑。如果你要接本地模型,那显存分配又是另一个话题,但就 Agent 编排层而言,一台普通开发机完全够用。

项目目录结构如下,整体保持简洁:

agent_reach/ ├── agent/ │ ├── __init__.py │ ├── core.py # 决策循环主逻辑 │ ├── llm.py # 模型接入层 │ ├── tools.py # 工具注册与执行 │ ├── registry.py # 工具注册表 │ └── audit.py # 审计日志 ├── config/ │ └── config.yaml # 模型、参数配置 ├── tools/ │ ├── log_tools.py │ └── notify_tools.py └── main.py # 入口

核心依赖只有三个:openai(兼容各家 API)、pydantic(参数校验)、yaml(配置解析)。整个项目的第三方依赖控制在个位数,这也是它能保持轻量的原因。

6.2 核心代码实现

先看配置,配置文件很直观:

model: provider: openai_compatible base_url: https://your-api-endpoint.com/v1 api_key: sk-xxxx model_name: qwen-plus temperature: 0.2 agent: max_steps: 8 parallel: true max_parallel: 4 history_keep_rounds: 5 audit: log_dir: ./logs

temperature我建议设置 0.2 左右。Agent 执行任务时我们希望它稳定、少发散,而不是像聊天一样脑洞大开。0.7 以上的温度会让工具选择变得飘忽不定。

核心循环代码较长,我把关键片段贴出来,重点看循环控制:

async def run_agent(task: str, tools: list[Tool]) -> str: messages = [{"role": "user", "content": task}] audit = AuditSession(task) for step in range(max_steps): response = await llm.chat(messages, tools) audit.log_step(step, response) # 模型没有工具调用,视为完成 if not response.tool_calls: return response.content # 执行工具调用(支持并行) results = await execute_tool_calls(response.tool_calls) # 将结果追加回上下文 for call, result in zip(response.tool_calls, results): messages.append({ "role": "tool", "tool_call_id": call.id, "content": result.summary_text }) audit.log_tool_call(call, result) # 上下文压缩 messages = compress_history(messages) raise AgentLoopLimitError(f"步骤超过 {max_steps} 次仍未完成")

这里有几个细节值得注意。我把工具执行结果转成summary_text再塞回上下文,而不是直接塞原始返回。因为原始返回可能非常大,比如日志查询工具可能返回几千行内容。我会在工具层做一层摘要,把最关键的信息提炼出来。日志工具返回时,默认只返回前 20 行、按级别统计的汇总、以及错误关键词列表。模型需要细节时可以再次调用工具去查,这样既保证了信息不丢,又控制了 Token 开销。

6.3 参数选择背后的思考

history_keep_rounds: 5这个值不是随便定的。太短,模型会忘记前面做过什么;太长,Token 浪费严重,而且工具输出里的噪声会影响模型判断。5 轮是我实测下来准确率和成本比较均衡的值。

max_parallel: 4也不是越大越好。并行工具调用对模型的要求更高,它需要同时为多个工具生成参数,出错率会上升。我试过 8 个并行,结果模型经常把两个调用的参数搞混。后来限制 4 个以内,准确率明显回升。

还有一个容易被忽略的配置:audit的日志目录。我强烈建议你把每一步的日志都保存下来。排查问题的时候,翻日志比让 Agent 复述发生了什么要靠谱得多。每一条日志我都会记录:输入消息、模型原始响应、解析后的工具调用、执行结果、耗时。这个对后面做评测、迭代 prompt、调整参数都非常重要。

7. 常见问题与排查技巧实录

7.1 问题速查表

这一节是从实际跑 Agent-Reach 过程中沉淀下来的踩坑记录。有些问题在网上搜不到答案,只能自己慢慢试。整理成表,方便你快速对照排查。

现象可能原因处理方式
模型反复调用同一个工具但没进展工具返回的信息不足以下判断检查工具返回摘要,补充关键信息;或增加max_steps
工具选错,用 A 工具做 B 的事工具描述太模糊重写工具描述,明确触发条件和边界场景
模型生成的工具参数格式不对参数 Schema 过于宽松增加参数描述、限制枚举值、强制类型校验
执行到一半上下文超长工具返回内容过大且未压缩在工具层做摘要;开启上下文压缩
并行调用中两个工具有依赖循环逻辑未做依赖检测实现简单的参数依赖检查,强制串行
Agent 明明完成了任务还继续跑模型没有收到"停止"信号检查最终回复是否使用了特殊的停止标记;在 prompt 里强调输出规范

7.2 我在排查中最常用的几个手法

如果你发现 Agent 行为不对,第一件事不是改 prompt,而是打开审计日志,完整看一遍模型每轮的思考轨迹和工具调用记录。很多"诡异行为"其实都有迹可循,只是你没看日志之前完全猜不到。

举个例子。我之前遇到的问题:Agent 经常给出特别敷衍的回答,比如"任务完成",但实际工作没做。翻日志发现,模型其实执行了工具,但工具结果返回的是空列表。模型判断"没有数据,所以完成了"。问题出在日志查询工具返回空列表时没有说明"没有匹配结果,可能关键词太严格或时间范围太短"。模型不知道还有没有其他可能,就直接停了。我在工具返回里加了一句提示:"如果没有结果,请尝试放宽关键词或扩大时间范围再查一次",这种行为立刻消失了。

再分享一个技巧:给模型提供"人在环上"的通道。不是所有任务都能全自动完成。当 Agent 遇到自己无法判断的情况,比如"是否要删除这 100 条记录",我让它停下来清晰地说明情况,然后人工确认。这个机制看起来简单,但在实际使用中非常重要,能避免很多不可挽回的操作。

还有一点,模型对"我不知道"的容忍度很低。它倾向于给一个看起来合理的答案,而不是承认自己不够了解。所以你需要在 prompt 里明确告诉它:遇到不确定的情况,可以调用一个"查询参考文档"的工具,或者直接请求人工介入。不要让它硬编一个答案。

7.3 值得养成的三个习惯

第一,每次迭代只改一个变量。不管是改 prompt、改工具描述、还是改 temperature,一次只动一个,然后跑同一批测试用例看差异。同时改两个变量,出了效果都不知道是哪个起的作用。

第二,建立回归测试集。收集 10-20 个典型任务,每次改完核心代码就跑一遍。Agent 项目非常容易出现"修好了一个问题,又带崩了另一个场景"的情况。没有回归测试,你会在不知不觉中被各种边缘 case 折磨疯掉。

第三,记录 Token 消耗。每个任务跑完,统计输入输出 Token 数、调用了几次工具。这样你能发现哪些任务经常做无用功。我曾经发现某个场景每次要跑 7 次工具调用,优化工具描述之后降到了 3 次,成本直接砍半。不记录就没法优化。

8. 基于 Agent-Reach 的实际场景与扩展方向

实战场景说了这么多,我用一个综合案例来总结整个项目的价值。假设我在一个业务团队里负责运维自动化。我接了一个需求:每天早上生成的错误日志,需要自动分析、归类、发给相关负责人。

用 Agent-Reach 做这件事,链路非常清晰:

  1. 写一个query_logs工具,负责从日志系统里拉数据
  2. 写一个classify_error工具,负责把错误分类
  3. 写一个send_notification工具,负责发消息
  4. 给 Agent 一个任务模板:"分析昨天的错误日志,按类型统计,给每个负责人发一条通知"

Agent 会自己去调工具、汇总数据、生成通知内容。整个过程完全可观测,出问题能定位到具体步骤。这套体系的真正价值不在于省掉了多少人工,而在于把专家经验沉淀成了可复用、可扩展的自动化流程。

后面我在 Agent-Reach 上还做了几个扩展方向。一是给工具调用加了缓存层,相同参数的工具调用在短时间内直接返回缓存结果,省 Token 也省时间。二是把审计日志接到了一套可视化看板上,直接在界面上看每一步的流转。三是给 Agent 加了一个"计划模式"——先让模型把任务拆成计划步骤展示出来,用户确认之后再执行。这几个扩展都不复杂,但实际使用体验提升非常明显。

在做 Agent-Reach 的过程中我最大的体会是:Agent 能不能真正落地,关键不在于模型强不强,而在于你愿意花多少功夫去调工具、调描述、调循环逻辑。模型是大脑,工具是手脚,循环是神经。这套系统要顺滑地运转,每一步都需要细致打磨。我也还在持续优化,后面有什么新发现再继续分享。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进