做Agent开发也有一段时间了,从最早跑通LangChain的Demo,到后来自己动手拼一个真正能落在业务里的Agent,最大的感受是:圈子里太多项目停留在“能聊天”的阶段,真正能把活干完、把结果交付出来的Agent,少之又少。这也是我做Agent-Reach这个项目的初衷——它不追求通用大模型的玄学效果,而是聚焦一件事:让AI Agent真正触达任务的终点。
Agent-Reach是一个轻量的Agent执行框架,核心思路是“规划—执行—反馈—修正”的闭环。它解决的核心问题是:当你给Agent一个真实任务(比如“把某个网页整理成Markdown归档”“从一堆文档里提取关键字段”),它能不能自主拆解、调用工具、处理异常、最终交付结果。适合正在学Agent开发的人、想自建Agent应用的技术团队,以及被LangChain这类重框架折腾过、想从底层理解Agent原理的开发者参考。这篇文章我会从架构设计、实操搭建、安全边界到扩展方向,完整复盘这个项目的来龙去脉。
1. Agent-Reach想解决的问题:为什么大多数Agent项目死在Demo阶段
1.1 花架子框架与真实业务之间的鸿沟
这几年Agent框架层出不穷,LangChain、Dify、CrewAI各有拥趸,但真正用起来你会发现一个尴尬的事实:Demo跑得飞起,一接真实业务就卡壳。我见过不少团队用Dify搭了一个“看起来能干活”的Agent,结果一让他处理几十个文件、跨多个工具协作、中途出个网络异常,整个流程就瘫在那里。原因很简单——框架本身只是把模型的输入输出包装了一下,它并没有真正解决“任务怎么被可靠地执行”这件事。
还有一个更基础的问题:很多所谓的Agent其实是伪Agent。你问他“今天天气怎么样”,他生成一段话,看起来像回答,但背后没有调用任何工具;你让他“查一下某公司的工商信息再做个摘要”,他直接凭空生成一堆看似合理但经不起核实的内容。这不是Agent,这是高级一点的文本补全。真正的Agent必须有工具调用、有环境反馈、有状态变化,否则它永远只是在“说”,而不是在“做”。
我做Agent-Reach时给自己定了一条底线:任何任务,如果没有产生一个可交付的产物(文件、记录、结构化数据、或一个明确完成的状态),就算失败。这一条看起来简单,但实际做下来会逼你解决很多框架不替你解决的问题。
1.2 Agent-Reach的适用范围和边界
Agent-Reach适合做什么?最适合的是中低频次的自动化任务,比如信息聚合、网页内容归档、文档抽取整理、定时生成简报、跨系统数据搬运这类“流程明确、但过程需要随机应变”的活。这类任务的共同点是:步骤之间可能有多种路径可选,但最终产物是明确的。Agent-Reach做的事情,就是让模型在每一条路径上做选择,同时保证每一步执行都有真实的工具在支撑。
不适合做什么?完全无人值守的高风险业务操作不建议直接上。比如自动付款、删数据库、发合同这种,就算理论上能做成,也要有人工确认闸门和严格的审计日志,这已经不是Agent层能解决的问题,而是业务流程治理问题。另外,如果目标是高并发的低延迟生产接口,那直接用Agent方案是资源浪费,任何一次工具调用都可能消耗几百毫秒甚至几秒,这类场景应该用确定性的代码处理,Agent只在复杂的非标链路里当决策节点。
技术选型上我用的是Python服务端加SQLite,模型侧通过OpenAI兼容接口对接,因此市面上的主流模型基本都能用。有人会问为什么不用Rust写——新一代Agent框架确实有不少Rust实现,性能和并发能力确实强,但生态还不够丰富,个人项目快速迭代时Python的效率优势太明显了。我的态度是:先让业务闭环跑通,再考虑用Rust重写核心热路径,这也是Agent-Reach后续计划中的一条线。
2. Agent-Reach的架构设计:五个核心模块怎么搭
2.1 大脑与手脚分离:Planner与Executor
Agent-Reach的第一条架构准则是:把“大脑”和“手脚”分开。所谓大脑是Planner模块,负责理解目标、拆解步骤、决定下一步动作;所谓手脚是Executor模块,负责真正去调用工具、获取结果、返回观测信息。把它们拆开的直接好处是——调试时你能清楚地知道一个问题到底是“想错了”还是“做错了”。
Planner不直接碰任何外部资源。它的输入只有任务目标、当前状态、可选工具列表,输出是一个明确的下一个动作。这个动作要么是“调用某个工具的某个参数”,要么是“宣告任务完成”或“宣告任务无法完成”。为了让模型稳定输出这种结构化动作,我一开始尝试过free-form文本让模型自由发挥,结果解析起来痛苦不堪,后来改成强制要求输出JSON格式的动作描述,用Pydantic做解析和校验,整个稳定性上了一个台阶。
Executor的逻辑则更接近传统编程:拿到动作描述,查工具注册表,做参数合法性检查,执行工具函数,把结果塞回上下文。真正写代码时这个模块其实不大,但它是Agent与外部世界交互的唯一通道,因此所有的安全检查都应该在这里做,而不是散落在各个工具函数里。
还有一个看似不重要但实战中极其关键的设计:任务拆解时,Planner需要判断子任务之间的依赖关系。如果两个子任务互不依赖,可以并行;如果后面的步骤依赖前面的产物,就必须串行。Agent-Reach目前的版本做了简化:默认串行执行,只有遇到“独立收集多个数据源”这类明确模式时才启用并行。为什么这么保守?因为并行会把上下文管理的复杂度放大好几倍,多个工具同时返回结果时,模型的注意力会被稀释,出错率反而上升。
2.2 记忆系统:短期Workbench与长期Memory Store
Agent的记忆问题,硬要归类其实是两层。第一层是对话记忆,也就是模型要记得用户之前说了什么;第二层是任务状态记忆,也就是Agent执行到哪一步了、已经产生了哪些中间结果。很多人做Agent只在第一层下功夫,结果任务稍微长一点,前面的步骤结果就被上下文窗口冲掉了,Agent开始“失忆”。
Agent-Reach里,我引入了一个叫Workbench的短期记忆区。它是一个进程内的键值存储,专门存放当前任务产生的中间变量。比如一个文档处理任务,前一步抽取出的表格数据会临时放在Workbench里,下一步要生成摘要时直接从Workbench读取,而不需要把整段原始数据反复塞进模型上下文。这样做的好处有两个:一是省Token,二是避免模型被大量中间数据干扰判断。
长期记忆则是跨会话的。Agent-Reach用SQLite加向量字段做了个简易的记忆库,存储对象是“任务类型+关键参数+结论摘要”。当下次用户提出类似任务时,Agent可以先去记忆库里检索历史做法,跳过重复摸索的过程。这个设计初期可以只做到“关键词精确匹配”,后续要升级成向量检索也很方便,把存储换成向量数据库或者SQLite的向量扩展即可。
记忆设计的核心原则是“只存取必要信息”。我见过不少人动不动把整个历史记录全部塞回上下文,Token烧得快不说,模型还容易在无关信息上产生幻觉。实际跑下来的经验是:每个工具调用的结果,先进行摘要压缩,只把压缩后的要点记入上下文,原始结果全部落到Workbench或本地文件里。
2.3 Harness(缰绳)机制:给Agent套上安全边界
Harness这个词用在Agent领域,现在讨论度很高,很多人问它和Agent本身有什么区别。我的理解是:Agent是“脑和手”,Harness是“缰绳和笼子”。一个Agent如果没有任何约束,就像一匹脱缰的马,能力强但方向不可控。Harness负责四件事:初始化上下文、维护工具注册表、执行终止条件判断、兜底处理异常输出。
举个例子。模型输出JSON动作时,经常会出现少个括号、多一个逗号、或者把参数名拼写错。刚开始跑Agent-Reach时,这类解析错误三天两头出现。后来我在Harness里加了一道自动修复程序:先用Pydantic解析,失败后尝试截取最外层JSON片段重新解析,再失败就调用一次模型让它“修正自己的输出”。这道三层兜底把解析成功率从90%出头提到了99%以上。
Harness里还要定义什么情况下必须停止。Agent-Reach设了三层终止条件:一是Agent自己宣告任务完成;二是累计工具调用次数超过上限(默认15次);三是连续多次动作没有产生有效进展(比如重复调用同一工具同一参数)。第三层尤其重要,模型有时会在一个失败的工具调用上反复横跳,如果没有这个机制,一次任务的Token消耗会失控。
3. Agent-Reach的实操搭建:从零到一跑通一个网页归档任务
3.1 环境准备与项目骨架
实操部分我用一个非常经典的案例来演示:把任意网页抓取下来,转换成Markdown,保存到本地归档。这个任务虽然简单,但覆盖了Agent的核心链路——拆解目标、调用工具、处理中间结果、产出最终文件。
项目依赖很简单,核心是OpenAI SDK(用于调用兼容接口的模型)、Pydantic(用于输出解析)、requests和BeautifulSoup(用于网页抓取与解析)、html2text(用于转Markdown)。整个项目目录我按标准的分层结构组织:
agent_reach/ ├── core/ │ ├── planner.py # 规划模块:模型调用与动作决策 │ ├── executor.py # 执行模块:工具注册与动作执行 │ ├── harness.py # 主控循环:初始化、终止、异常兜底 │ └── memory.py # 工作台与记忆库 ├── tools/ │ └── web_tools.py # 网页抓取和转换工具 ├── skills/ │ └── archive_page.md # 技能描述文件 └── main.py # 入口模块之间的依赖关系是单向的:main调用harness,harness持有planner和executor的引用,executor从tools目录加载工具,planner从memory读取上下文。这样拆的好处是任何一个模块都可以单独替换和测试,尤其是工具层,后面想加多少工具都不需要改主流程。
模型接口我统一走OpenAI兼容的Chat Completions格式,只需要在配置里指定base_url和api_key,就能切换不同的后端模型。这种兼容层设计非常实用,因为你永远不知道项目后面会换到哪个模型供应商,提前做一层抽象能省掉后续大量改造工作。
3.2 核心实现:从工具注册到Agent主循环
先写工具。每个工具是一个普通的Python函数,但需要附带一份声明信息,包括名称、描述、参数Schema。这个声明会被Harness拼进系统提示词里,让模型知道有哪些工具可用、各自长什么样。
from pydantic import BaseModel import requests import html2text from bs4 import BeautifulSoup class FetchPageParams(BaseModel): url: str def fetch_page(params: FetchPageParams): """抓取指定网页的HTML内容,返回纯净的正文HTML""" resp = requests.get(params.url, timeout=15) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") for tag in soup(["script", "style", "nav", "footer", "header"]): tag.decompose() return str(soup.body) class ToMarkdownParams(BaseModel): html: str def to_markdown(params: ToMarkdownParams): """把HTML正文转为Markdown格式""" converter = html2text.HTML2Text() converter.ignore_images = False return converter.handle(params.html)工具声明写好了,接下来是关键的主循环。Agent-Reach的主循环非常朴素——循环里做三步:让模型决策、执行动作、记录结果。判断结束的条件是模型输出了done动作,或者触发了之前说的终止保护。
def run_agent(task: str): system_prompt = build_system_prompt() # 包含工具声明、任务约束、输出格式 context = [{"role": "system", "content": system_prompt}, {"role": "user", "content": task}] for step in range(MAX_STEPS): # 1. 让模型决策下一个动作 response = call_llm(context) action = parse_action(response) # 解析JSON动作,带三级兜底 # 2. 执行动作 if action.type == "call_tool": result = execute_tool(action.tool_name, action.params) context.append({"role": "user", "content": f"工具返回:{summarize(result)}"}) elif action.type == "done": return action.result else: context.append({"role": "user", "content": "当前动作无法执行,请换一种方式"})这段代码虽然短,但它体现了Agent的核心循环逻辑。我特意把工具返回的结果先做一遍摘要再放入上下文,这是省Token的关键。原始返回内容仍然会完整保存在Workbench里,后续如果需要,Agent可以再调用一个“查看中间数据”的工具去读取细节。
实际操作中你会发现,模型真的会在合适的时机调用工具并完成任务,但也会出现很多奇怪的用法。比如有的模型会连续调用twice同样的参数,或者明明可以一次完成的步骤非要拆成三次工具调用。这些都要靠上一节提到的Harness保护机制来约束,而不是指望模型自觉。
3.3 参数调优与Token预算控制
跑通主循环之后,最值得花时间调的就是模型参数和成本控制。温度参数我一开始用默认的1.0,结果模型经常在工具选择上“发挥创意”,一会儿想用不存在的工具,一会儿在参数里填一些看似合理的假数据。后来我把temperature调到了0到0.2之间,整个确定性直线上升。Agent任务不是写作题,不需要创造性,它的目标是稳定执行。
Token预算方面,我用这个网页归档例子实测过:一个中等长度的新闻网页,整个Agent流程下来大概消耗1.2万到1.8万Token。其中真正花在“处理网页内容”上的只占一小部分,大头全在多轮工具调用的对话历史上。所以我做了一个很极端的优化:每执行完一个工具步骤,就把那一步之前的历史做一个滚动摘要,用一两句话概括“已经完成的事情”,然后把详细过程丢给本地存储。这样上下文长度始终被压在一两万Token以内,无论任务多长都能保持稳定。
def condense_context(context): if len(context) > 12: # 超过12条消息就压缩 summary = call_llm("请用两句话总结我们已完成的工作", context) return [{"role": "system", "content": summary}, context[-2:]] return context这个压缩策略看着简单,实际效果却非常好。我跑过最长的一个任务,折腾了将近40个工具调用,上下文最终也没有超过两万Token,而且Agent的思路一直清晰,没有出现“遗忘前面步骤”的问题。如果你自己搭Agent,建议无论如何要加一道类似的上下文管理逻辑。
4. 安全与可靠性:Agent沙箱、权限校验与失败恢复
4.1 工具调用的三道安全闸门
Agent的安全感不来源于模型是不是靠谱,而来源于工程上能不能兜底。我给自己定了一个铁律:即使模型完全放飞,Agent-Reach也不能产生不可控的副作用。为此设计了三道闸门。
第一道是工具白名单。模型只能调用注册过的工具,任何未注册的函数都无法被executor执行。这个不用多说,属于地基级设计。第二道是参数校验。虽然每个工具的参数用了Pydantic定义,但Pydantic只保证类型正确,不保证值合法。比如一个文件删除工具,参数里填了“/etc/passwd”也是合法字符串,所以还要在工具内部或executor层加业务规则校验,比如路径必须限定在某个根目录下、URL必须使用白名单域名、批量操作数量不能超过某个阈值。
第三道是人工确认闸门。对于有外部副作用且不可逆的操作,Agent不能直接执行,而是生成一个“待确认动作”,把完整参数展示给用户点击确认后才真正执行。这道闸门是业务风险的最后防线,特别是接钱、发消息、删数据这类场景,永远建议保留人工确认环节。
4.2 沙箱环境:什么情况下真的需要沙箱
热词里Agent沙箱讨论不少,我的判断是:不能一刀切,要看Agent被赋予了什么能力。如果Agent只能调用你写好的JSON Schema严格定义的API,那沙箱不是必需品;但如果Agent有执行任意代码的能力,或者需要处理来自不可信来源的输入,那沙箱就是救命的。
Agent-Reach目前的默认设计是“不执行任意代码”——所有动作都落到预定义工具上。但扩展方向上,我预留了沙箱接口:把Agent的代码执行类工具放到一个独立容器里运行,容器的文件系统、网络、资源都有隔离和限制。用Docker做这件事最省事,把执行脚本挂载进容器,在外面抓取输出即可。
docker run --rm \ --network none \ -v ./workspace:/workspace \ --memory=256m \ --cpus=0.5 \ python:3.11 \ python /workspace/execute.py上面这个命令的核心是--network none,禁止容器访问网络。很多安全问题其实都是网络出去才造成的,断了网沙箱的安全性就高了一大截。实测下来这种做法很稳,既保留了执行环境,又大幅缩小了攻击面。
4.3 失败恢复与重试策略
Agent跑生产任务,失败是常态,关键是失败之后怎么恢复。我把Agent-Reach的失败分成三类逐一处理。
工具本身的执行异常,比如网络超时、第三方API返回500,这类直接捕获异常并让模型知道“这次调用失败了,原因是什么”,模型通常会自己换一种方式重试。模型输出解析失败,靠Harness的自动修复兜底,前文已经说过。还有一类是逻辑层面的失败,比如模型连续调用同一个工具拿不到想要的结果,这时候需要靠“连续动作无进展”保护来判断,干脆终止任务,给出半成品状态报告,让用户决定下一步怎么处理。
def is_stuck(history): recent = history[-3:] if len(recent) < 3: return False return all(a.tool_name == recent[0].tool_name and a.params == recent[0].params for a in recent)排查Agent问题时,最重要的手段是日志。Agent-Reach会把每一步的工具调用、参数、返回结果摘要、Token消耗全部记录到结构化日志里。遇到“Agent execution terminated due to error”这种报错时,先把日志按时间线拉出来,通常一眼就能看出来是模型决策错了、工具参数传错了,还是外部环境不稳定。记住,调试Agent本质上不是调试程序逻辑,而是调试“模型在每一步看到了什么、为什么做出这个决策”,所以上下文的历史记录比代码更重要。
5. Agent Skills与多Agent协作:再向前一步
5.1 Skill机制:把任务能力打包成可复用资产
工具解决的是“单个动作”,Skill解决的是“完整任务”。工具是fetch_page,Skill可以定义为“把网页保存为Markdown归档”——它内部协调多个工具,还包含提示词模板、参数约定、处理流程说明。这个设计现在被越来越多的Agent项目采纳,本质是把经验固化下来,避免每次任务都从零开始探索。
我在Agent-Reach里定义Skill的方式非常简单:一个Markdown文件加一个参数Schema。Markdown里写清楚这个Skill的触发场景、执行步骤、注意事项和输出格式;参数Schema用JSON描述。Harness把Skill描述注入系统提示词,模型遇到匹配任务时先加载Skill,再按Skill里定义的流程执行。
--- name: archive_webpage description: 将网页内容转换为Markdown并保存到本地归档 params: url: string category: string --- 执行步骤: 1. 使用fetch_page获取网页HTML 2. 使用to_markdown转换为Markdown 3. 使用save_archive保存,文件名按日期和分类组织 注意事项: - 如果网页内容过长,只保留正文部分 - 保存前确保分类目录已存在这个Skill定义方式是我参考了当前主流Agent Skills的思路后设计的,重点在于“让模型先读一遍说明书再动手”。实测效果是:一线Agent的首次任务成功率从不到60%提升到85%以上,因为模型不再需要靠猜才能知道任务怎么执行。
5.2 多Agent协作:主管加执行者的主从模式
单Agent能力再强,也有其边界。任务一旦涉及多个专业方向,比如既要写代码又要做数据分析还要核实事实,最好的方式不是让一个Agent干所有的活,而是让多个Agent分工协作。Agent-Reach采用的简化模式是“主管—执行者”:主管Agent负责理解用户目标、拆解子任务、分发下去;执行者Agent每个专注于一个领域,做完交回结果;最后由主管整合产出。
这个模式最大的优势是每个Agent的上下文都很干净。执行者不需要知道整个任务的全貌,只管好自己的一亩三分地,因此失误率会显著下降。代价是通信成本高——每次主管分发任务和执行者汇报都得来一轮模型调用,Token消耗会翻倍甚至更多。
编排逻辑可以比喻成带团队:主管要做的是把任务说得足够清楚、验收标准明确,执行者才可能一次做对。我优化过的经验是,分派子任务时一定要附带“什么叫完成”的定义,否则执行者很容易自认为完成而其实只做了一半。
5.3 评测集构建:怎么验证Agent真的变强了
最后想聊聊评测。做了Agent开发之后,你会发现最痛苦的不是写代码,而是不知道自己的改动到底是变好了还是变坏了。模型一旦升级、提示词一旦修改、工具行为一旦变化,整个任务的完成率就跟着波动。没有一套评测集,一切都只能凭感觉。
Agent-Reach里我建了一个很轻量的评测集,格式是JSON数组,每个元素包含任务描述、期望产出路径、关键校验点和预估用时。跑评测时,让Agent顺序执行所有任务,最后统计三个指标:任务完成度、工具调用效率(平均一个任务用了几次工具)、失败模式分布。
| 任务编号 | 任务内容 | 完成情况 | 工具调用次数 | 失败原因(如有) |
|---|---|---|---|---|
| 01 | 抓取指定网页并转Markdown归档 | 完成 | 4 | 无 |
| 02 | 从某页面提取标题和正文摘要 | 完成 | 3 | 无 |
| 03 | 根据网页内容生成FAQ清单 | 部分完成 | 6 | 输出内容偏长,未按格式分点 |
每组跑完看结果曲线,就能很客观地判断这次改动是正向还是负向。我现在的习惯是,每次改进完跑一轮十八到二十个任务的评测集,大概需要十几分钟,但这十几分钟能省下后面几天的反复试错。评测集本身也要持续迭代,把线上看到的失败案例不断补充进去,Agent的开发过程就会变成一个稳步收敛的过程。
我个人在实际操作中最深的体会是:做Agent不能太迷信模型本身的能力,工程体系才是决定上限的关键——上下文管理、工具约束、异常兜底、评测回归,每一环都比“提示词写得更巧”更重要。刚开始跑通一个流程时你会觉得不可思议,等到样样问题都见过了,回头再看,Agent其实就是一个带了大语言模型决策器的传统系统,所有工程原则依然适用,只是多了一层需要耐心应对的随机性。
如果这个项目继续往下走,我下一步的计划是把核心循环用Rust重写,把工具执行的并发能力和内存占用优化上去,再把评测集构建成一个更完整的小工具分享出来。目前这套代码已经完全能够支撑我日常的网页归档、文档整理和信息汇总任务,也希望这个复盘能给正在Agent开发路上折腾的朋友一点参考。