Agent-Reach实战:构建能交付结果的Agent执行框架
发布时间:2026/10/8 5:11:57 作者:尧图编辑部 阅读量:1,286

做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与ExecutorAgent-Reach的第一条架构准则是把“大脑”和“手脚”分开。所谓大脑是Planner模块负责理解目标、拆解步骤、决定下一步动作所谓手脚是Executor模块负责真正去调用工具、获取结果、返回观测信息。把它们拆开的直接好处是——调试时你能清楚地知道一个问题到底是“想错了”还是“做错了”。Planner不直接碰任何外部资源。它的输入只有任务目标、当前状态、可选工具列表输出是一个明确的下一个动作。这个动作要么是“调用某个工具的某个参数”要么是“宣告任务完成”或“宣告任务无法完成”。为了让模型稳定输出这种结构化动作我一开始尝试过free-form文本让模型自由发挥结果解析起来痛苦不堪后来改成强制要求输出JSON格式的动作描述用Pydantic做解析和校验整个稳定性上了一个台阶。Executor的逻辑则更接近传统编程拿到动作描述查工具注册表做参数合法性检查执行工具函数把结果塞回上下文。真正写代码时这个模块其实不大但它是Agent与外部世界交互的唯一通道因此所有的安全检查都应该在这里做而不是散落在各个工具函数里。还有一个看似不重要但实战中极其关键的设计任务拆解时Planner需要判断子任务之间的依赖关系。如果两个子任务互不依赖可以并行如果后面的步骤依赖前面的产物就必须串行。Agent-Reach目前的版本做了简化默认串行执行只有遇到“独立收集多个数据源”这类明确模式时才启用并行。为什么这么保守因为并行会把上下文管理的复杂度放大好几倍多个工具同时返回结果时模型的注意力会被稀释出错率反而上升。2.2 记忆系统短期Workbench与长期Memory StoreAgent的记忆问题硬要归类其实是两层。第一层是对话记忆也就是模型要记得用户之前说了什么第二层是任务状态记忆也就是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调用harnessharness持有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, timeout15) 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 \ --memory256m \ --cpus0.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_pageSkill可以定义为“把网页保存为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开发路上折腾的朋友一点参考。