ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从会聊到会办事:智能体中间层设计与工具调用实践

2026/9/8 5:20:38 拓冰建站 浏览量
从会聊到会办事:智能体中间层设计与工具调用实践 从“会聊”到“会办事”我把智能体做成了信使做了一年多的大模型应用落地我最大的感受是现在做一个会聊天的机器人已经不难了难的是让这个机器人真的去干活。你可以让它写文案、做总结、翻译文档但一旦涉及到“帮我查一下库存再算一下运费顺便给客户回封邮件”这种多步操作单靠提示词工程根本无从下手。后来我把思路换了一下——与其在提示词里堆砌所有逻辑不如把大模型放回它最适合的位置理解意图、拆解任务、调用工具。而真正串起整个流程的中间层就是我给项目起的名字hermes-agent。Hermes是希腊神话里的信使神负责传递信息、引导灵魂穿越边界。我觉得这个名字特别贴切一个负责任务分发、信息传递、过程编排的智能体中枢本质上就是大模型与外部世界之间的信使。这篇文章主要聊聊我实现hermes-agent时的整体设计、核心模块拆解、实际落地过程中踩过的坑以及为什么我认为这种架构会是AI应用的主流形态之一。不吹概念只讲能跑起来的东西希望能给正在搞智能体的朋友一些参考。1. 整体设计为什么需要中间调度层而不是硬写代码1.1 大模型不是流程引擎它是决策器很多人在第一次接触LangChain或AutoGPT这类框架时会有一种错觉把一堆工具塞给Agent它就能自己解决所有问题。实测下来不是这样。模型确实能调用工具但在复杂任务面前很容易出现“上下文越长决策越飘”的情况——工具调错了、参数拼错了、中间步骤丢三落四这些都是常见问题。我设计hermes-agent时先定了一个原则大模型只在关键节点做决策流程本身由代码控制。举个例子你让Agent执行“查一下这个客户的订单状态然后起草一封催款邮件”。在这个流程里模型只需要做两件事识别出要查哪个订单以及判断邮件语气是否合适。至于“先查订单再写邮件”这个顺序逻辑应该由代码固定下来否则让模型自由发挥它可能先写邮件再去查订单甚至自以为已经完成了查询。所以我做的是一个轻量的中间调度层负责四件事解析用户意图、编排执行步骤、调起对应工具、把每步结果汇总并反馈给模型。模型在整个环节里扮演的是指挥官而不是搬砖的。1.2 核心模块消息队列、工具注册表、会话记忆hermes-agent的整体结构并不复杂内部核心模块可以分成三层。最底层是消息队列所有指令和结果都通过队列流转。这样做的好处是Agent实例和执行器之间完全解耦便于后续做分布式部署。实际开发时我直接用了Python标准库里的queue模块配合asyncio实现异步分发并没有一开始就引入Celery或Kafka这类重型组件——对大多数中小型项目来说先让系统跑通比一上来就上分布式重要得多。中间层是工具注册表和指令规划器。工具注册表解决“模型知道有哪些工具可用”的问题每个工具声明名称、描述、参数JSON Schema模型看到这些声明后按格式输出调用意图。指令规划器则负责把大目标拆成可执行的小步骤这一步是核心后面我会细讲。最上层是会话记忆和状态管理。Agent得记住用户刚才说了什么、当前做到哪一步了、哪些变量已经从工具结果里拿到了。这块我一开始低估了难度后来发现状态管理不做好Agent做三步以上的任务时经常出现前后矛盾。1.3 对比直接硬编码和纯LangChain方案做这个项目之前我其实试过两种路线这里给大家做个对比。硬编码路线很好理解就是if-else写死流程只适合单一场景。好处是可预测、绝对可控但每接一个新需求就得改代码而且业务逻辑一变整个分支结构都得动。纯LangChain路线可以把工具定义得很灵活但真正做复杂业务时你会发现框架帮你做的事越多出问题时越难排查。最重要的是LangChain版本迭代太快今天写的代码下个月可能就换API了维护成本很高。hermes-agent走的是一条中间路线不依赖重型框架必要时我甚至直接调OpenAI/国产大模型的原始接口。核心调度逻辑自己写工具接入用统一协议业务层完全与模型供应商解耦。这样既保证了灵活性又能按自己的需求去定制行为。2. 核心细节解析Agent怎么知道该做什么、怎么做2.1 意图识别与指令规划的边界划分大家看各种Agent演示时很容易被“智能”两个字迷惑。实际上Agent在跑一个复杂任务时靠的是“意图识别 指令规划 工具调用”三步循环。意图识别这一步我用的方式是让模型输出一个结构化的JSON里面包含action动作类型和payload动作参数。比如用户说“把上个对话里提到的报价单转成PDF发给李总”模型会输出类似{ action: execute_pipeline, payload: { steps: [ {tool: find_file, params: {keyword: 报价单}}, {tool: convert_to_pdf, params: {}}, {tool: send_email, params: {recipient: 李总}} ] } }不要小看这个设计。过去很多Agent框架里模型可以自由调用工具然后自己判断下一步等于每次都是一个开放式的聊天。而通过要求模型先输出一个完整的步骤数组Pipeline相当于让模型在动手之前先做计划我再在代码层校验每个步骤的参数是否齐全。从实测效果看这种先规划再执行的模式任务成功率比“逐步自主决策”高出不少。2.2 工具注册表让模型“知道”有哪些工具可以用模型本身并不知道你的系统里有哪些工具它只能根据提示词里的描述来理解。所以工具注册表设计得好不好直接影响模型调用的准确性。每个工具注册时需要提供四段信息name工具名、description描述这个工具干什么、什么时候用、parameters参数JSON Schema、handler真正干活的函数。模型通过描述来匹配意图然后按照Schema生成参数。这里我踩过不少坑。最开始我的参数Schema写得太松比如查库存的接口只声明了“product_id”模型经常把产品名称直接扔进来。后来我把Schema写详细加上枚举值、格式示例甚至标注“如果用户只说了产品名先调用search_product接口查询ID”。这样一个看似微小的调整工具调用的成功率从60%左右提升到了90%以上。2.3 会话记忆与状态管理的两个坑Agent做多轮交互时最麻烦的就是状态管理。我一开始把所有历史消息一股脑塞给模型让它在里面自己找需要的信息结果有两个问题。第一个是token消耗太大。一个10轮以上的会话历史文本快顶上一天的量了。第二个更麻烦当历史太长时模型会“遗忘”最早的关键约束条件。比如用户第一轮说“只要华东区的数据”聊到第六轮时Agent可能就把华东区这个过滤条件忽略了。后来我采用了一种很实用的方案把状态信息从会话历史里抽出来单独维护一份上下文快照。快照包含当前任务的关键变量、已经完成的步骤列表、待执行的步骤列表每次调用模型时快照作为系统提示的一部分输入其余的闲聊历史才作为对话历史传给模型。这样既能控制token长度又能保证关键约束不丢。3. 实操过程从零搭一个能跑起来的Agent3.1 项目骨架与最小实现下面直接讲代码我用的Python 3.10核心依赖只有两个openai或国产模型的SDK和pydantic不用重型框架。目录结构大概是这样的hermes-agent/ ├── agent_core/ │ ├── __init__.py │ ├── planner.py # 指令规划器负责生成步骤数组 │ ├── executor.py # 步骤执行器逐个执行planner生成的步骤 │ ├── registry.py # 工具注册表维护所有可用工具的元信息 │ ├── memory.py # 会话记忆与状态快照 │ └── router.py # 消息路由连接用户输入和Agent核心 ├── tools/ │ ├── __init__.py │ └── basic_tools.py # 内置几个示例工具 └── config.py # 模型API key、模型名、参数配置等Executor的执行逻辑很简单本质上就是一个循环遍历每个步骤根据工具名从注册表找到handler传入参数拿到结果。如果某一步执行失败会把错误信息追加到上下文里让模型重新规划。3.2 关键代码Planner如何生成步骤数组Planner的核心提示词我没有用太复杂的模板反而是越简洁越有效。关键在系统提示里明确告诉模型你是任务规划器只能输出JSON不能输出多余内容。planner_prompt 你是任务规划器。你的职责是将用户指令拆解为可执行的步骤。 可用的工具列表如下 {tools_description} 注意 1. 步骤必须按逻辑顺序排列后面的步骤可以依赖前面的输出。 2. 参数值如果暂时无法确定使用变量占位符如 $output.step1.file_id。 3. 你必须只输出JSON格式为 {{steps: [{{tool: 工具名, params: {{...}}, requires: [上一步的变量名]}}]}} .format( tools_descriptionregistry.describe_tools() )这里有个小细节值得说明requires字段。它声明了当前步骤依赖哪几步的输出。执行器会先检查依赖是否满足如果不满足就报错让模型重新规划。这一招避免了模型“跳步”的毛病——它知道自己得先拿到文件ID才能发邮件而不是凭空编一个ID。3.3 执行器与工具联动原理执行器其实是个很机械的东西但它承载了容错、重试、超时控制这些关键能力。我封装了一个run_step方法async def run_step(self, step, context): tool self.registry.get(step[tool]) if tool is None: raise ToolNotFoundError(step[tool]) # 解析参数支持从context中取变量 params self._resolve_params(step[params], context) try: result await asyncio.wait_for( tool.handler(**params), timeouttool.timeout ) return result except asyncio.TimeoutError: return {error: 工具执行超时}工具Handler的返回值我强制为JSON可序列化的dict然后统一写回context。后面生成回复时模型是可以看到这些JSON的。因为所有结果都是结构化的模型回答最终问题时就有据可依不太会出现胡说八道的情况。3.4 本地实测用3个工具跑通一次完整任务为了让大家更直观地理解整个过程我搭了三个示例工具做了一次模拟测试一个“获取当前时间”的工具一个“计算日期差”的工具还有一个“读取本地Markdown文件”的工具。我输入的指令是“今天是几号请读取项目目录下的README.md然后计算今天与该文件最后修改日期之间隔了多少天。”执行过程如下Planner解析意图识别出需要三个步骤依次是获取当前日期、读取文件的修改时间、计算日期差。Executor执行第一步拿到今天的日期写入context。Executor执行第二步读取文件元信息拿到修改日期写入context。Executor执行第三步把两个日期作为参数传给计算工具得到差值。最后生成自然语言总结今天是几月几日文件是什么时候修改的相差多少天。整个过程跑下来大约3秒。速度主要取决于模型返回JSON的延迟三轮交互耗时较长但因为工具本身很快体验还可以接受。4. 多Agent协作当单一智能体不够用时怎么办4.1 需要多个Agent的业务场景单一Agent能做的事总归有限。比如我给企业做内部工具时发现一个Agent很难同时做好“对话交互”和“数据查询”这两件事——对话交互需要模型理解自然语言、生成拟人回复而数据查询则需要严格的参数校验和确定性逻辑。把两者混在一个Agent里模型会在两边反复横跳。后来我在hermes-agent内部做了一个主Agent加多个子Agent的结构。主Agent负责理解用户需求判断该把任务发给哪个子Agent比如“日期计算请发给计算Agent”“邮件发送请发给邮件Agent”然后汇总结果返回用户。这种模式在自动化办公场景里尤其好用。4.2 消息队列的分发逻辑多Agent之间的通信我同样用消息队列来实现。每个子Agent有一个专属队列主Agent把封装好的任务丢到对应队列子Agent消费后执行并回传结果。分发逻辑最怕的是循环依赖。比如A Agent需要B的结果B Agent又需要A的结果两边就卡死了。我在设计里做了一层防护任务消息里带了一个max_depth字段每经过一次分发就减一减到零直接报错。这样即使有循环调用也能快速暴露问题。4.3 任务优先级与并发控制多Agent并发时还要处理好两个问题优先级和限流。优先级方面我用的是优先级队列高优任务比如用户主动询问会插队执行后台批处理任务排在后面。限流方面每个Agent每秒最多处理N个请求防止一次性涌入大量任务把API打满。这些参数都放配置文件里不必改代码就能调整。这里特别提醒一下如果你的Agent要调用大模型API做实时推理务必做好并发上限控制。我上线第一版时没注意这个问题结果在内部测试中一次性提交了50个任务直接把API额度打爆了最后被运维同事拉去聊了一下午。5. 常见问题与排查技巧实录5.1 模型返回JSON不稳定解析老失败这是被问得最多的问题。模型输出的JSON偶尔会带json代码块标记或者多几行解释文字直接json.loads就会报错。我自己用的解决方法是写一个宽松的解析层先尝试直接解析失败则用正则去掉代码块标记再取第一个{到最后一个}之间的内容。这属于经验性的兜底方案。如果你发现模型频繁输出无效JSON还有一个办法在系统提示里加一句“你只能输出一行合法的JSON禁止输出解释”实测能减少80%的解析失败。5.2 Agent在长任务执行中“失忆”长任务执行到一半模型会忘记最初的目标。比如用户要求“先查A再对比B然后生成报告”前三步还好到第四步生成报告时可能就把对比的基准忘了。我的排查结果是问题出在上下文里塞了太多工具返回的中间结果把原始指令挤到后面去了。解决办法有两个方向。第一在每次给模型发请求时把原始指令固定在系统提示里并且要求模型在最终报告里显式引用原始要求。第二规划器生成步骤时就把目标拆得足够细每一步的输入输出都明确标注这样哪怕模型后面 “失忆”执行器按步骤走也能走到终点。5.3 工具并行执行时的脏数据有些步骤之间并没有依赖关系理论上可以并行执行。我一开始直接用了asyncio.gather并发跑结果发现多个工具同时写入context时有些变量被覆盖了。定位后发现是共享变量的问题。解决方式是给context分了命名空间每个工具只写自己的前缀比如email_client.last_response、storage.file_list。这样即使并发执行也不会互相踩脏数据。5.4 排查工具链路的通用思路最后聊一下排查问题的通用思路。很多朋友遇到Agent行为异常时第一反应是调提示词我建议反过来先看链路数据再动提示词。我自己的经验是在Executor每个步骤入口和出口都打一条结构化日志记录工具名、输入参数、输出摘要、耗时。排查问题时直接看日志里的步骤序列就能知道是规划错了、工具报错了还是参数解析错了通常一分钟定位问题比反复试不同的提示词高效得多。写在最后的一点建议我个人在实际项目里最大的体会是Agent的真正难点不在单点技术而在于整体架构的分层与解耦。只要调度层稳定、工具接入规范、状态管理清晰后面加新功能基本就是注册一个新工具的事。如果你也想做类似的项目建议从一个小场景切入比如让Agent帮你操作某个内部系统里的一两个动作查数据、发消息、改个字段。先跑通这个最小的闭环再慢慢扩展工具和其他Agent。不要一上来就追求“全自动”那样大概率会陷入提示词调试的泥潭。hermes-agent目前还在持续迭代下一步我打算把工具调用失败后的自动重试策略做得更智能让它在遇到临时性错误比如接口超时、token限流时能自己等一会儿再试一次。这块打磨好了Agent在真实生产环境里的可用性还能上一个台阶。