ARTICLE DETAIL

资讯详情

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

从零搭建智能体框架:Agent-Reach的触达链路设计与实践

从零搭建智能体框架:Agent-Reach的触达链路设计与实践 前阵子把一个内部项目命名为 Agent-Reach骨架搭完、跑通第一轮端到端任务之后不少朋友来问这个项目到底做了什么、难点在哪里、怎么从零把它搭起来。借着这个机会我把整个设计和实操过程梳理了一遍。这篇文章不会去堆概念就是围绕 Agent-Reach 这个项目讲清楚它解决的痛点、核心模块的实现方式以及我实际踩过的坑。无论你是准备自己写一个智能体框架还是想把手头的 LLM 调用封装成更可控的 Agent 系统这篇都值得花几分钟看完。1. 项目概览与技术定位1.1 Agent-Reach 在解决什么问题Agent-Reach 这个名字拆开看就两个词Agent 和 Reach。Agent 指的是智能体Reach 强调的是触达能力。整个项目的核心目标就是解决一个非常实际的问题大语言模型本身只是会说话的脑子它要真正完成任务必须能够触达外部世界——调用工具、读取数据库、请求第三方 API、操作业务系统。Agent-Reach 就是这一层触达能力的开关和管道。我最早动手做这个项目是因为团队里几个业务方频繁提同一个需求能不能让大模型直接去查订单状态、自动同步库存、根据条件生成报表然后推送到钉钉群。单独看每一个需求无非就是调 API、拼 prompt、解析结果。但数量一多问题就来了每个接口都有一段拼 prompt 的胶水代码工具返回的数据格式五花八门模型偶尔还会产生幻觉乱调用工具。Agent-Reach 要做的就是把这一层统一收口提供一套可复用的模型到工具的触达机制。和市面上那些重框架相比Agent-Reach 的定位更轻。它不追求把 LangChain 那一整套抽象全部复刻一遍而是专注于三个核心问题模型该怎么稳定地决定调用哪个工具、工具结果该如何被结构化地回传、多轮对话中上下文和记忆该如何维护。这三件事做好就已经能覆盖大部分真实业务场景了。1.2 为什么把触达作为核心设计主线在设计 Agent-Reach 的时候我把触达拆成了四个层面。第一层是模型触达也就是对各类 LLM 的接口调用包括超时控制、重试策略、流式输出。第二层是工具触达所有业务能力都被封装成工具函数统一注册、统一调度。第三层是数据触达包括外部数据库查询和内存态的记忆读取。第四层是用户触达也就是结果通过什么方式送达用户端比如控制台日志、API 推送、消息机器人。这四个层面如果分开做每个都不难但合在一起就成了一个完整的闭环。Agent-Reach 从设计第一天起就按这个四层模型组织代码这样做的好处是每一层都能独立测试、独立替换。比如模型层今天用 GPT明天要切到国产模型或者本地部署的模型只需要换掉一层适配器工具调度和记忆模块完全不用动。我见过很多智能体项目挂掉不是模型能力不够而是触达链路太长太脆。链路里任何一个环节出问题——模型返回格式非 JSON、工具超时、上下文把关键信息挤掉了——整个任务就断了。Agent-Reach 的主线设计就是要把这条链路每一个环节都变成可控的、可观测的、可恢复的。2. 环境搭建与技术选型2.1 技术栈清单与版本选择Agent-Reach 的技术栈不复杂核心就是 Python 3.11 FastAPI Pydantic。选 Python 不用多说Agent 生态的工具基本都是 Python 写的。3.11 版本主要是为了兼顾性能和异步支持实测 3.12 也行但有些第三方库还是对 3.11 兼容更稳没必要为了追新给自己添麻烦。FastAPI 承担两个职责对外提供 HTTP 接口让业务系统能够把任务提交进来对内提供异步运行时让多个 Agent 任务能并发执行而不互相阻塞。Pydantic 是最关键的依赖之一它承担了结构化输出的校验和解析。模型返回的 JSON 如果不符合工具调用的 schemaPydantic 会立刻抛错我们就能做重试或修正而不是稀里糊涂地把坏数据传给工具。LLM 接入方面我一开始写了一个通用的 OpenAI 兼容适配器。现在大多数模型服务商都支持 OpenAI 格式的请求只需要改 base_url 和 api_key 就能切换。本地开发我主要用 Ollama 跑 qwen2.5 之类的小模型来调试基本流程验证无误后切到云端大模型做完整任务。这么做的好处是调试链路的成本几乎为零而且不会因为模型 API 限流而影响开发节奏。Redis 是可选项用来做跨进程的会话状态存储。单机单进程开发的时候可以省掉但一旦要多实例部署会话数据就不能放在内存里Redis 是最轻的解决方案。日志和监控选了 Loguru Prometheus 客户端这俩也算标配不用多说。2.2 项目目录设计与基础配置目录结构我在第一个版本就定好了因为后边每次调整功能都会动到目录前期定清楚能少踩很多组织混乱的坑。Agent-Reach 的主目录如下agent-reach/ ├── app/ │ ├── core/ # Agent 生命周期、调度主循环 │ ├── models/ # 数据模型、工具 Schema、会话模型 │ ├── tools/ # 工具注册表、内置工具、外部工具适配器 │ ├── memory/ # 短期记忆、长期记忆、摘要压缩 │ ├── adapters/ # LLM 适配器、数据库适配器 │ └── api/ # FastAPI 路由、请求校验 ├── tests/ # 单元测试与集成测试 ├── configs/ # YAML 配置 └── logs/ # 运行时日志这个目录生长的逻辑是core 负责流程tools 负责能力memory 负责状态adapters 负责外部连接。新人接手项目的时候看目录就能定位到对应模块不需要翻文档。基础配置我放在 configs/config.yaml 里。核心参数如下llm: provider: openai_compatible base_url: http://localhost:11434/v1 model: qwen2.5:7b temperature: 0.2 max_tokens: 1024 timeout: 30 agent: name: agent-reach-default max_iterations: 8 planning_strategy: simple_plan tool_mode: parallel memory: short_term_size: 10 summary_threshold: 2000 summary_model: qwen2.5:7b tools: registry_size: 20 allowlist: [] # 空表示全部可用 denylist: [] server: host: 0.0.0.0 port: 8000 max_concurrent_tasks: 20注意几个参数的用意。temperature 我设到 0.2这个值在 Agent 场景下特别关键工具调用的决策必须稳定不要说这次调用 weather.get下次因为温度太高改成 weather.forecast导致下游逻辑崩了。max_iterations 是 8这是防止 Agent 陷入死循环的兜底手段后面会在实际运行中验证它的必要性。tool_mode 默认用 parallel也就是一次思考后可以同时调多个工具这对查询类任务的效率提升非常明显。3. 核心机制拆解一个 Agent 到底怎么跑起来3.1 Agent 生命周期任务解析、规划、工具调度、结果合成Agent-Reach 最核心的循环我把它拆成五个阶段任务解析、规划、工具调度、反馈整合、结果输出。这五个阶段构成一个循环循环终止条件有两个一是 Agent 认为自己已经完成了任务二是达到了 max_iterations 上限。任务解析阶段系统会把用户输入的自然语言转成一个结构化的任务对象。包括任务类型、关键参数、期望产出格式。比如用户说帮我看下北京明天适合穿什么衣服任务类型就是 weather_query参数是城市北京、日期明天期望产出是穿衣建议。这一步不依赖模型做数学推理只需要模型做信息抽取所以稳定性很高。规划阶段我会让模型基于任务对象列出子步骤。用 prompt 的方法迫使模型输出结构化的规划列表再通过 Pydantic 校验。这个阶段要特别注意的是不要让模型把规划做得太长超过五步的规划在真实场景中八成是过度规划。我在 prompt 里明确限制了规划步骤数超过就截断并提示模型简化。工具调度阶段是 Agent-Reach 的重头戏。模型输出的工具调用意图会经过一层意图校验器这层校验器检查三件事工具是否存在、参数是否符合 schema、权限是否允许。很多大模型会编造不存在的工具名这层校验能直接拦截。合法的调用意图才会真正执行执行结果统一包一层 ActionResult包含状态、数据、耗时。反馈整合阶段就是把工具返回的数据、模型自身的观察、可能出现的错误信息一起拼接成新一轮的上下文。合成结果阶段如果是多步任务Agent 需要把所有中间结果汇总生成一份对用户有意义的最终回答。这五步首尾相连构成了 Agent-Reach 单次会话的主循环。3.2 工具调度层把外部能力变成结构化接口工具调度层是 Agent-Reach 里我投入最多精力打磨的部分。每个工具在注册时需要一个 JSON Schema 描述包括工具名、描述、参数结构、返回值结构。这个 Schema 不仅仅是给程序看的它会被直接组装进发给模型的 prompt —— 模型就是依赖这些描述来学会什么时候该调用哪个工具的。工具注册使用装饰器语法本质上是一个全局注册表。我举个例子from pydantic import BaseModel from app.tools import register_tool class WeatherQueryParams(BaseModel): city: str date: str register_tool( nameweather.get, description查询指定城市某一天的天气情况返回温度、降水概率和风力, params_schemaWeatherQueryParams, ) def get_weather(params: WeatherQueryParams) - dict: # 这里是真实的业务调用逻辑 ... return {temperature_max: 28, temperature_min: 21, precip_prob: 30, wind_level: 3}这里有一个很容易被忽略的要点工具描述不是写给程序员看的是写给模型看的。描述写得越具体模型越不容易产生歧义。有些工具的边界条件也要在描述里写清楚比如本工具只支持中国主要城市以下城市不支持……这能有效减少模型因为信息不足而乱猜的情况。执行过程中的错误处理同样重要。我封装了一个 execute_tool 方法内置超时控制、并发限制、异常捕获。工具执行崩溃不会让整个 Agent 会话挂掉而是会把异常信息结构化成执行结果回传。代价是增加了代码复杂度但换来的是极高的稳定性——把出错的工具当成返回了错误信息的正常工具来对待Agent 就能在下一轮思考中自我纠正。3.3 记忆模块滑动窗口加摘要压缩记忆模块在 Agent-Reach 里分两层。短期记忆是一个消息列表保存最近 N 轮对话N 默认是 10。长期记忆则依赖摘要压缩当消息总数超过 summary_threshold 时触发一次总结把之前的对话压缩成一段摘要然后清空旧的完整记录。这个设计借鉴了 LLM 上下文管理的通用技巧。直接全量塞上下文有两个问题一是 token 消耗大成本成倍翻二是距离窗口中间的内容容易被模型忽略反而干扰决策。用摘要 最近对话的组合既能保住核心信息又能让模型把注意力集中在最新的上下文。我在实现摘要压缩时踩过一个坑让模型做摘要的时候一定要强调摘要里必须保留工具返回的关键结构化数据。第一次实现时摘要生成器把对话压缩得特别流畅结果发现某次对话里已经查到的订单号被压缩掉了后续步骤无从下手。后来我在摘要 prompt 里加了硬性要求所有数字、编号、名称、代码块必须原文保留。这一条规则效果立竿见影。4. 实战环节从零搭建一个最小可用 Agent4.1 第一步实现 Agent 主循环现在进入实操部分。我把 Agent-Reach 的主循环核心代码贴出来这部分是整个项目的中枢。下面的代码去掉了细枝末节保留了核心链路。class AgentReach: def __init__(self, config: dict): self.config config self.memory ConversationMemory( short_term_sizeconfig[memory][short_term_size], summary_thresholdconfig[memory][summary_threshold], ) self.llm LLMAdapter.from_config(config[llm]) self.tools ToolRegistry() self.max_iterations config[agent][max_iterations] async def run(self, user_input: str, session_id: str None) - AgentResult: self.memory.load_session(session_id) self.memory.add_user_message(user_input) for iteration in range(self.max_iterations): # 1. 构造 prompt包含消息历史与工具描述 prompt self._build_prompt() # 2. 调用模型得到结构化响应 response await self.llm.chat(messagesprompt) # 3. 判断是否结束 if response.is_finish(): final_answer response.content self.memory.add_assistant_message(final_answer) return AgentResult(statussuccess, answerfinal_answer) # 4. 解析工具调用意图并执行 tool_calls self._parse_tool_calls(response) if not tool_calls: # 模型意图不明确让它重新表达 self.memory.add_system_message(请明确说明下一步工具调用意图。) continue tool_results [] for call in tool_calls: result await self._execute_tool_call(call) tool_results.append(result) # 5. 把工具结果回填到上下文 self.memory.add_tool_results(tool_results) # 6. 检查是否超轮次 if iteration self.max_iterations - 1: return AgentResult(statustimeout, answer任务超时未能完成全部步骤。) return AgentResult(statusfailed, answer任务执行失败) def _build_prompt(self) - list[dict]: messages [ {role: system, content: SYSTEM_PROMPT}, ] messages.extend(self.memory.get_context_messages()) messages.append({ role: user, content: 可用工具列表 self.tools.get_tool_descriptions() }) return messages async def _execute_tool_call(self, call: ToolCall) - ToolResult: try: tool self.tools.get(call.tool_name) if tool is None: return ToolResult(statuserror, errorf工具 {call.tool_name} 不存在, sourcevalidator) validated_params tool.validate_params(call.params) if validated_params is None: return ToolResult(statuserror, error参数校验失败, sourcevalidator) raw await tool.execute(validated_params) return ToolResult(statussuccess, dataraw, sourcecall.tool_name) except Exception as e: return ToolResult(statuserror, errorstr(e), sourceexecutor)这个主循环在功能上是完整的。每轮迭代做的事情很清晰组织输入、调用模型、判断结束、解析工具调用、执行工具、回填结果。实际使用中可以把循环体的每一步都打日志这样跑完一个任务后整个思考-动作-观察的链条完全透明。4.2 第二步接入天气查询作为第一个工具为了验证主循环的正确性我接入了一个最简单的天气查询工具。它不是真实的外部 API先用内存固定的假数据返回等链路走通了再换真实 API。这是典型的模拟先行做法好处是排除了外部网络波动对调试的干扰。工具注册代码在 3.2 节已经展示过。这里补一个关键点工具返回的数据格式必须符合注册时声明的 schema模型在后续推理中会依赖这个数据。Agent-Reach 的处理方式是在工具实现外层包一层 response_model 校验。返回数据如果校验失败会被标记为 malformed绝不流入上下文。接入工具后我给模型发了一条指令查询北京明天的天气如果降水概率超过 40%提醒我出门带伞。 这虽然是个简单任务但已经覆盖了完整链路任务解析、一次工具调用、结果判断、生成最终回答。跑通这一步相当于验证了 Agent-Reach 的触达管道是通的。4.3 第三步跑通端到端任务并观察轨迹第一次跑通完整任务时我特意开启了 verbose 日志模式。下面是一次真实的运行轨迹片段[迭代 1] 用户: 查询北京明天的天气降水概率超过40%就提醒我带伞 Agent 规划: [调用 weather.get 获取北京明日天气, 根据降水概率生成提醒] 工具调用意图: weather.get(city北京, date2026-02-15) 工具结果: {temperature_max: 26, temperature_min: 18, precip_prob: 60, wind_level: 4} [迭代 2] Agent 观察: 降水概率为60%超过40%需要提醒用户带伞。 最终回答: 北京明天白天最高26度夜间最低18度降水概率60%建议出门带伞。这条轨迹看起来很朴素但背后包含了几个关键细节模型第一轮就成功生成了符合 schema 的工具调用参数工具结果被结构化解包第二轮模型基于工具返回的数据做出了正确判断。这三点成立说明 Agent-Reach 的触达链路是健康的。我在测试过程中还会故意制造一些故障来观察 Agent 的恢复能力。比如把工具改成先抛异常看看模型能不能在下一轮澄清需求或者切换备用工具。实践下来效果最稳定的是错误回传 提示重试。模型在看到工具返回的 error 字段后通常会重新审视参数或者明确告诉用户目前无法完成。这比简单崩溃优雅得多。5. 常见问题与排查实录5.1 高频报错排查速查表在 Agent-Reach 的开发过程中我积累了一些高频问题的排查经验。下面这个表格对刚接触 Agent 开发的人应该很有用。问题现象大概率原因解决方式模型工具调用参数乱填工具 Schema 描述不够规范重写工具描述明确每个参数的类型、取值范围、必填性模型调用不存在的工具工具名单没有清晰暴露给模型检查 system prompt 中工具列表是否完整且词表顺序固定工具返回 JSON 解析失败外部 API 返回格式与 schema 不一致在工具层做数据清洗增加响应校验拦截非法数据任务跑了几轮后开始复读上下文窗口中间信息被稀释缩短长期摘要保留关键数据原文模型明明有工具可用却拒绝调用模型认为自己可以直接回答在 system prompt 中加入使用工具优先的策略描述一次任务反复循环不终止缺少终止条件的强制约束调低 max_iterations或增加连续两轮无新信息时强制结束逻辑这几个问题里我遭遇频率最高的是工具调用参数乱填。一个小技巧值得分享在工具 Schema 的字段描述里把格式直接写死比如日期字段写格式必须是 YYYY-MM-DD禁止使用今天、明天这类相对日期。这句话对模型的约束力比在代码里做五十行日期解析都管用。5.2 稳定性、并发与成本调优Agent-Reach 跑了一段时间后稳定性的瓶颈不在模型本身而在外部工具和链路管理。最终我总结出三条调优经验。第一条是并发控制在工具层而非 Agent 层。同一个 Agent 会话内的多个工具调用我采用 Promise.all 并发执行但不同会话之间我用了一个简单的信号量限制最大并发数避免瞬间打出大量外部请求被限流。这个信号量的初始值我设成 20看起来保守但配合请求排队机制整体吞吐反而稳定了很多。第二条是重试策略必须分级。LLM 调用的超时重试跟外部 HTTP API 的重试策略不能一样。前者通常适合作 2 次快速重试因为模型服务恢复快后者要根据 API 的幂等性判断如果 API 不保证幂等重试反而可能造成重复操作。Agent-Reach 的做法是工具声明自己的幂等性非幂等工具在失败时优先走人工介入流程。第三条是成本控制依赖上下文剪枝。Agent 跑的步骤越多token 消耗指数增长。我做了两件事第一系统提示词和工具描述统一走缓存不重复计算第二中间步骤的工具结果不做全文保留而是提取关键字段后再入上下文空间。这两步加起来同样任务的 token 消耗大约降了三分之一。对高频场景来说省下来的费用相当可观。6. 后续扩展与个人心得6.1 扩展方向多智能体、流式输出、AgentOpsAgent-Reach 目前的形态是单 Agent 任务的执行框架但它预留了几个扩展点我接下来的规划也围绕这些方向展开。第一个方向是多智能体协作。当任务颗粒度变大比如生成一份周报并发送到指定群单个 Agent 需要同时掌握数据查询、模板渲染、消息推送三种能力prompt 会被撑得很难维护。更合理的做法是拆成三个子 Agent由调度 Agent 统一协调。Agent-Reach 的工具注册表只需要增加一个 agent_tool 类型就能让一个 Agent 调用另一个 Agent 的能力架构上的改动很小。第二个方向是流式输出。现在的实现是拿到最终完整结果后一次性返回用户等待体验不好。我正在把主循环改成生成器模式每完成一个子步骤就 push 一条进度事件。这样用户的画面上可以实时显示正在分析任务正在查询天气正在生成回复交互质量会有质的提升。第三个方向是 AgentOps 观测。日志记录做得再多都不如一套可视化的 trace 面板直观。我计划把每轮迭代的工具请求、模型输出、耗时、token 消耗都埋点输出到 Prometheus再在 Grafana 上拉一张看板。这样当线上任务出问题时就能一眼看出是模型决策慢了还是某个工具接口 500 了。6.2 几点真话踩坑之后总结的实操铁律Agent 开发跟传统后端开发的思维方式差别非常大。在我自己调试 Agent-Reach 的过程中有几点体会最深刻。第一别把稳定性押在模型一定会听话上面。模型再强也会犯格式错误、也会产生幻觉工具调用。Agent 框架存在的意义就是给模型的行为加上一层工程护栏——schema 校验、异常捕获、超时兜底、终止条件一个都不能少。这些护栏本身才是稳定性的核心模型只是决策器。第二日志观测能力决定了开发效率。没有逐步日志的 Agent 就像黑盒出了问题只能靠猜。Agent-Reach 从第一天就把可观测写进了设计目标。每轮迭代都会输出结构化日志包含输入摘要、工具调用、结果摘要、耗时。调试的时候看着这个轨迹问题基本一眼就能定位。第三工具质量决定 Agent 上限。同一个任务工具描述写得准确模型一次就调用成功写得不准确可能要来回试三次。花在工具定义上的时间最终都会以更稳定的链路和更低的消耗回报回来。我在 Agent-Reach 里所有工具描述都至少改过三版每一次打磨都是值得的。Agent-Reach 这个项目到目前为止已经从一个玩具原型变成了能支撑真实业务任务的轻量框架。它的代码量不大但每一处设计都是为了解决实际工程问题而存在。如果你也在做类似的 Agent 落地探索可以从这个思路出发试试。把自己的一个工具接进来跑通一轮端到端任务你大概就能理解这套设计的价值所在了。
返回列表