Lagent+AgentLego智能体开发:工具调用与多智能体编排

发布时间:2026/10/26 15:13:42
Lagent+AgentLego智能体开发:工具调用与多智能体编排 上个季度有个做内部系统的朋友抱着一台笔记本找我说他手头攒了七八个能力接口——图片识别、语音转写、表格查询、网页抓取每个单独跑都挺顺可一旦想让模型自己判断先用哪个、再用哪个整条链路就开始乱套模型要么把参数编错要么在同一个工具上反复来回要么干脆不按约定的格式输出。这其实是绝大多数人第一次做智能体应用时都会撞上的墙。我后来给他换了个思路把思考调度和能力实现彻底分开推理循环、工具选择、上下文管理交给 Lagent各类多模态能力和业务接口统一用 AgentLego 封装成标准工具两边靠一层薄薄的适配器接起来。这篇就把这套方案从环境搭建、工具注册、提示词设计、多智能体编排到线上排查的完整过程写清楚包含大量我实际踩过的坑和参数取舍。适合已经会调模型接口、想往能连续行动的应用再走一步的开发者也适合被各种智能体概念绕晕、想找一条能跑通的最小路径的朋友。1. 为什么是Lagent加AgentLego先拆需求再选框架1.1 一个能落地的智能体到底需要哪几块能力先把智能体这个词扒开看你会发现它没那么玄。一个能真正干活的智能体拆到底就四层第一层是模型会话负责和推理服务对话管理消息格式、温度、最大长度这些基础参数第二层是行动循环模型输出我要调用某个工具框架负责解析、执行、把结果塞回上下文再让模型继续判断第三层是工具集合每个工具都要有名字、用途描述、参数定义和返回值约定第四层是运行保障包括超时、重试、日志、上下文裁剪、错误回传。这四层里最容易被低估的是第三层——很多人以为工具就是写个函数实际上框架能不能正确调用它八成取决于你写的函数描述而不是函数体本身。我见过不少自研项目把四层全揉在一个脚本里前期跑 Demo 很爽等到工具数量涨到十个以上、需要并行调用、需要多轮纠错的时候整个文件就变成了一团解不开的线。所以我的建议很直接如果你打算长期维护就不要自己从零写这套骨架找一个成熟的推理循环框架加一个工具封装层把精力省下来放在业务逻辑和提示词上。这也是我最后落到 Lagent 与 AgentLego 这套组合上的直接原因。1.2 Lagent管想AgentLego管做这两个组件在我眼里是明确分工的。Lagent 提供的是智能体骨架它有 ReAct 这类思考—行动—观察循环的实现有统一的工具抽象基类有消息与结果的标准化结构还内置了若干种 Agent 形态可以直接拿来改。你只需要告诉它有哪些工具可用用哪个模型最多循环几轮剩下的事情它自己转。而 AgentLego 解决的是另一侧的问题它把图像描述、目标检测、图像分割、文本转语音、图像生成这类多模态能力以及网页解析、文件处理这类通用能力统一包装成带标准描述和参数结构的工具对象并且自带向主流智能体框架转换的适配方法。分开的好处非常实际。第一是复用同一个工具对象既能在单智能体里用也能塞进多智能体的执行器里不用为每个场景重写一遍。第二是隔离工具的依赖通常又重又杂有的要显卡、有的要外部服务把它们和推理循环解耦之后我可以先在本地小规模验证循环逻辑再逐步把重工具挂上去。第三是调试粒度出问题时我能立刻判断是模型选错了工具还是工具自己报错而不是对着一整块黑盒猜。1.3 什么时候别用这套组合说优点也得说边界不然就是软文了。如果你的需求只是单轮问答、或者固定的三步流水线先抽取再分类再生成那根本不需要智能体框架写几个顺序函数调用比什么都稳引入行动循环只会增加不确定性和费用。如果你的工具全部是纯文本的轻量接口、对多模态完全没有需求那 AgentLego 的价值也有限直接继承 Lagent 的工具基类手写就行。还有一个场景要特别小心对延迟极度敏感、要求响应在几百毫秒内的实时接口不要用自由循环的智能体因为模型思考几轮的时间完全不可控这时候应该把流程固化用规则或状态机来驱动。我的经验判断标准是当任务步骤无法提前穷举、需要根据中间结果决定下一步并且允许秒级延迟时才值得上智能体。这条线划清楚后面很多架构争论都能省掉。2. 环境准备与项目骨架搭建2.1 依赖安装与版本对齐安装这一步看着简单其实是最容易翻车的地方。Lagent 和 AgentLego 都在快速迭代两边的工具基类接口偶尔会有变动最稳妥的做法是固定版本并且优先用源码安装以便随时改源码。我通常建一个独立虚拟环境Python 版本选 3.10 左右太新的版本有时候会撞上某些推理库还没出预编译包的问题。python -m venv venv source venv/bin/activate pip install -U pip setuptools wheel # 两个组件都建议固定版本避免接口漂移 pip install lagent0.2.* pip install agentlego0.2.* # 如果需要多模态能力再按需装推理后端 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121这里有个细节值得说AgentLego 的多模态工具背后往往挂着一个具体模型不同工具依赖的模型权重和推理后端不一样。我的做法是先用 CPU 或小模型把整条链路跑通确认工具注册、参数传递、结果解析都正确再换成大模型上显卡。因为绝大多数调不通的问题其实出在链路而不是模型能力先排查链路能省下大量等待模型加载的时间。注意不要在同一环境里同时装多个推理后端的不同版本冲突起来报错信息会非常迷惑看起来像是工具本身的问题实际是底层库版本打架。2.2 目录结构与配置文件项目一大配置就会失控。我习惯的骨架大概是这样一个configs目录放模型和工具的配置一个tools目录放自定义工具一个agents目录放智能体组装逻辑一个runs目录存每次执行的完整轨迹。之所以单独把轨迹落盘是因为智能体调试几乎全靠回放——你必须能看到模型每一轮看到了什么、输出了什么、工具返回了什么否则根本无从下手。project/ ├── configs/ │ ├── model.yaml │ └── tools.yaml ├── tools/ │ ├── __init__.py │ └── biz_query.py ├── agents/ │ ├── __init__.py │ └── single_agent.py ├── runs/ └── main.py配置文件里我坚持两件事模型参数和工具清单分开写密钥走环境变量绝不进代码。工具清单用列表描述每项包含工具名、是否启用、关键参数这样换环境时只需要改一个文件不用去翻代码里散落的初始化语句。2.3 模型服务接入的三种姿势Lagent 对模型接入的抽象是只要符合聊天补全协议就能接所以实际有三条路可选。第一种是直接调远程兼容接口最省事适合快速验证缺点是对网络和额度有依赖第二种是用本地推理服务起一个兼容端点把模型跑在自己的机器或内网机器上好处是数据不出域、延迟稳定代价是要自己管显存和并发第三种是写一个极简的自定义模型类只实现接收消息列表、返回文本这一个方法适合接入内部已有的推理网关。我个人在开发阶段用第一种跑通逻辑上线前换成第二种或第三种。切换成本很低因为智能体层只关心给我一段文本不关心文本从哪来。唯一需要注意的是不同模型的输出风格差异很大——有的模型特别听话严格按格式输出有的模型喜欢加一堆解释文字导致解析器抓不到动作。所以模型切换之后提示词和解析逻辑一定要重新回归测试一遍别假设换模型是零成本的。3. 把AgentLego工具挂进Lagent3.1 工具注册的底层逻辑工具注册这件事本质上是把一段 Python 能力翻译成模型能理解的三样东西一个短名字、一段自然语言描述、一份参数结构。模型看不到你的代码它只能看到这三样然后判断该不该用、怎么填参数。所以注册流程的核心不是把函数塞进去而是把函数的信息表达清楚。AgentLego 的工具对象本身已经带了标准化的名字、描述和参数定义它还提供了向智能体框架转换的方法转换之后就是一个符合 Lagent 工具基类规范的对象可以直接放进智能体的工具列表。from agentlego.apis import load_tool from lagent.agents import ReAct # 加载一个多模态工具device 可按需指定 caption_tool load_tool(ImageDescription, devicecuda:0) # 转成智能体框架可用的工具对象 lagent_tools [caption_tool.to_lagent()] agent ReAct( llmllm, actionslagent_tools, max_turn6, )这段代码短但里面有三个可以调的旋钮device决定工具跑在哪max_turn决定最多循环几轮工具列表决定模型的能力边界。我在实际项目里会把max_turn卡得比较紧一般 5 到 8 轮因为超过这个数还没收敛基本说明任务描述有问题或者工具不够用继续转下去只是烧钱。3.2 自定义工具从编写到跑通现成工具覆盖不到的業務逻辑就得自己写。写自定义工具时我遵循一个固定模板继承工具基类在run方法里做参数校验、调用真实服务、把异常转成结构化错误返回。返回结构一定要统一成功时带结果文本失败时带错误原因和状态码因为智能体需要靠这个判断要不要换个思路。from lagent.actions import BaseAction from lagent.schema import ActionReturn, ActionStatusCode class QueryOrderStatus(BaseAction): 查询订单当前状态输入订单号返回状态与最近一次更新时间。 def __init__(self, client, timeout: int 5): super().__init__() self.client client self.timeout timeout def run(self, order_id: str): if not order_id or not order_id.strip(): return ActionReturn( typetext, result订单号不能为空, errmsginvalid argument, stateActionStatusCode.ARG_ERROR, ) try: data self.client.get_status(order_id.strip(), timeoutself.timeout) return ActionReturn( typetext, resultf订单 {order_id} 当前状态{data[status]}更新时间{data[updated_at]}, errmsgNone, stateActionStatusCode.SUCCESS, ) except Exception as e: return ActionReturn( typetext, resultNone, errmsgf查询失败{type(e).__name__}, stateActionStatusCode.API_ERROR, )写完代码只是第一步真正决定成败的是类文档字符串和参数注解。工具描述里我坚持写三件事这个工具能做什么、输入长什么样、返回什么。参数注解里尽量用明确类型不要用过于自由的字典因为模型对自由结构的填写准确率会明显下降。我做过对比同一个工具把参数从一个包含订单号和时间范围的字典改成两个独立字符串参数之后调用成功率提升非常明显原因很简单结构越扁平模型越不容易漏字段。3.3 多模态工具的参数设计与返回裁剪多模态工具有个特殊性输入可能是一张图、一段音频返回可能是长文本甚至一个新的文件路径。这里最容易出问题的是返回内容太长直接塞回上下文一轮就把窗口撑爆。我的处理办法是在工具内部做一次裁剪和摘要只把关键信息返给模型完整结果存到本地并返回一个引用标识。比如图像描述工具如果原始输出有几百字我会截断到一两句核心描述同时把完整结果落到runs目录。还有一个细节是路径问题。多模态工具往往需要接收本地文件路径而模型可能编一个不存在的路径出来。稳妥的做法是在工具入口做一次存在性校验并且在工具描述里明确告诉模型路径必须来自上游返回结果不要自行构造。这条提示看着像废话但它实实在在减少了一类高频错误。4. 智能体核心循环提示词、记忆与行动4.1 ReAct循环里到底发生了什么理解循环的内部机制比调参数重要得多。一次完整的 ReAct 循环大概是这样的框架把系统提示词、工具清单、用户问题拼成消息发给模型模型输出一段文本里面包含思考过程和动作指令框架用解析器从文本里抠出动作名和参数执行对应工具把执行结果作为观察追加到消息历史再次请求模型直到模型给出最终答案或达到最大轮数。每一步都是一次完整的模型调用也就是说五轮循环就是五次推理成本和时间都要按这个算。明白了这个结构很多问题就有了解释。为什么模型会重复调用同一个工具因为它的观察结果里没有拿到它想要的信息或者工具返回的错误信息它没看懂。为什么解析会失败因为模型输出的格式和解析器约定不一致。为什么轮数会失控因为缺少停止条件模型不知道什么时候该收手。所有的优化都应该针对这几个具体环节而不是笼统地调温度。4.2 提示词模板的三段式写法提示词我固定分三段身份与目标、工具使用规范、输出格式约束。第一段说清楚你是谁、你要解决什么问题、什么情况下应该停下来第二段说明工具的选择原则和参数填写注意事项第三段严格定义输出格式。你是任务执行助手需要根据用户问题决定是否调用工具。 可用工具 {tool_descriptions} 使用规范 1. 每轮最多调用一个工具参数必须来自用户输入或已获得的观察结果。 2. 工具返回错误时先判断是否参数有误可修正后重试一次仍失败则向用户说明。 3. 信息足够回答时直接输出最终答案不再调用工具。 输出格式严格遵守 Thought: 你的简短推理 Action: 工具名 Action Input: {参数名: 参数值} 或者 Thought: 你的简短推理 Final Answer: 给用户的完整回答实测下来有三点体会。第一格式示例一定要给全包括Final Answer的分支很多解析失败是因为模型只会模仿动作格式、不知道怎么结束。第二每轮最多一个工具这句话要写否则有的模型会在一轮里输出多个动作解析器直接崩掉。第三工具描述的顺序也有影响把最常用的工具放前面模型的注意力会更集中选择准确率有肉眼可见的差别。4.3 记忆管理与上下文裁剪对话一长上下文就会爆。我的策略是分层最近三轮的完整对话原样保留更早的内容压缩成一段摘要工具产生的超长输出只保留关键结论。摘要本身也交给模型做提示词大概是用三句话概括以下对话中的事实、结论和待办。这样处理之后一个几十轮的任务也能稳定跑在窗口内。有一点要提醒摘要会丢信息所以涉及具体数值、编号、路径这类不能模糊的内容我单独维护一个结构化的事实槽位每轮都原样附在消息里不参与压缩。这相当于给智能体配了一个小记事本比指望它记住所有细节可靠得多。掉过一次坑之后这个习惯我一直保留着——那次任务跑了十几轮最后重试时订单号被摘要成了某个订单直接导致查询失败。5. 多智能体协作的编排思路5.1 什么信号说明你该拆多个智能体不是所有任务都值得拆成多智能体。我的判断标准有三个一是工具数量超过十五个单个智能体选工具时开始频繁选错二是任务可以明显分成几个阶段每个阶段需要的工具和知识完全不同三是需要检查环节即一个角色产出、另一个角色挑刺。三条里满足两条我会考虑拆分否则坚持单智能体加更清晰的提示词因为多智能体带来的通信成本、调试难度和费用都是成倍增长的。拆的时候也要克制两到三个角色足够覆盖大部分场景。常见的三角色是计划者、执行者、检查者计划者把用户需求拆成步骤清单执行者按步骤调用工具检查者对照原始需求核对结果完整性。这个结构的好处是每一步职责单一提示词可以写得很短很准不容易跑偏。5.2 调度器与消息传递的最小实现多智能体的核心是一个调度器。最简单的实现就是一个循环把当前任务和上一位角色的输出交给下一位角色收集结果判断是否满足结束条件。消息传递我推荐用一个统一的结构包含发送者、接收者、内容、状态四个字段这样日志回放的时候一目了然。调度器里必须有熔断比如单个角色连续失败两次、或者总轮数超过上限就强制结束并返回中间结果而不是无限转下去。def run_workflow(task, roles, max_steps12): history [] current task for step in range(max_steps): role roles[step % len(roles)] output role.act(current, history) history.append({role: role.name, input: current, output: output}) if output.get(done): return output[answer], history current output[content] return 任务未在限定步数内完成以下是中间结果 str(history[-1]), history这段伪代码看着简陋但把关键点都包住了角色轮转、历史记录、结束信号、步数上限。生产环境再加并发控制和超时控制就够了。我试过一上来就写复杂的图编排结果发现九十 percent 的问题跟图结构无关都是单个角色的提示词没写好先把角色写好再谈编排。5.3 一个可复现的计划执行检查Demo举个具体例子让智能体处理一份带图片的文档输出结构化摘要。计划者先把任务拆成提取文本、描述图片、合并摘要三步执行者拿到步骤后先调文本处理类工具再调图像描述工具把结果按步骤组织检查者对照原始要求看是否漏了图片、是否有字段为空有问题就打回执行者重做一次。整条链路里执行者才需要挂大量工具计划者和检查者的工具列表可以是空的只做文本推理。这样做的好处是工具只在必要的地方暴露选错的概率大幅下降。实测这套结构的稳定性明显高于单智能体硬扛代价是调用次数大概翻了一倍。所以我会在任务复杂度不高的时候果断退回单智能体别为了架构好看而多花钱。6. 常见故障排查与速查表6.1 工具调用失败的六类原因调不通的时候别急着换模型先按这张表过一遍八成问题能定位。现象最可能的原因排查动作模型从不调用工具工具描述太模糊或与问题无关检查描述是否说明什么时候用调用工具但参数为空参数注解不清晰或结构过深改成扁平参数并补示例参数值凭空编造描述未强调来源约束在规范里写明不得自行构造解析器报格式错误输出格式示例不完整补全结束分支示例同一工具反复调用观察结果没提供所需信息检查工具返回是否含关键字段工具报错后循环不止错误未结构化回传统一返回状态码与原因这张表是我自己踩坑总结的出现频率从高到低基本就是这个顺序。特别说第一行很多人以为模型不调工具是模型笨实际上九成是工具描述写成了查询订单状态这种名词短语模型根本不知道什么时候该用。改成当用户询问订单进度、物流状态时使用本工具之后调用率立刻上来了。6.2 死循环、超时与上下文爆掉死循环的典型特征是同一组动作反复出现观察结果几乎一样。根因通常是两个一是工具返回的信息不足以让模型推进二是提示词里没有明确的停止条件。我的处理是双保险框架层设最大轮数提示词层写清楚信息足够就输出最终答案并且在检测到连续两轮动作完全相同时强制中断把已有结果整理后返回。超时问题多出现在多模态工具上模型加载慢、推理慢一不小心就卡住整条链路。解决办法是给每个工具设独立超时超时后返回一个明确的错误状态让模型自己决定是重试还是换路。上下文爆掉则要靠上一节说的分层记忆另外把工具返回做截断别把原始的大段输出直接塞回去。6.3 延迟与成本的实测优化延迟主要花在模型调用次数上所以优化的第一优先级是减少轮数而不是换更快的模型。我做过一轮对比把系统提示词里冗余的寒暄和重复说明砍掉三分之一平均轮数从 4.6 降到 3.2整体耗时下降接近三成。第二优先级是工具本身的响应速度把耗时的工具做异步或者缓存重复查询直接命中缓存。第三才是模型选型简单任务用小模型跑循环、复杂任务才切大模型这个分级策略对成本的影响比任何单点优化都大。成本控制上我还有个习惯给每次执行打上任务类型标签定期统计各类任务的平均轮数和平均 token 消耗找出最贵的几类重点优化。数据摆出来之后优化方向基本不用猜。7. 实测体会几个不被文档提起的细节7.1 工具描述比工具实现更影响成功率这句话我重复多少遍都不嫌多。同样一个函数描述写成处理数据和写成当用户提供了一段原始文本、需要抽取其中的日期和金额时使用本工具输入为原始文本字符串调用准确率能差出一大截。我现在写工具的第一件事不是写代码而是先把描述念一遍看一个不了解业务的人能不能听懂什么时候该用它。如果念着别扭说明描述还得改。参数也是同理宁可多写两个独立参数也不要塞一个嵌套结构模型对嵌套的耐受力比想象中低得多。另外一个小技巧在描述里写清楚不适用场景。比如本工具只处理文本不处理图片这一句话能挡掉相当一部分误调用。模型其实很像一个刚入职的同事你交代得越具体它越不容易跑偏。7.2 日志与回放机制值得一开始就做智能体最难的是复现问题。同样的输入今天跑通明天跑挂如果没有完整轨迹你根本不知道差在哪。所以我在项目第一天就把每次执行的完整消息序列、工具调用参数、工具返回、耗时全部落盘格式用 JSON Lines一行一次调用方便用脚本统计。回放的时候可以逐轮打印看清楚模型每一轮到底看到了什么。这个投入的回报非常高。有次线上出现偶发失败靠回放发现是某个工具在特定输入下返回了空字符串模型拿到空观察之后开始胡乱重试。如果没有轨迹这个问题可能要找一整天。有了轨迹十分钟定位改完加个空值校验就解决了。7.3 上线前的检查清单最后分享我每次上线前必过的一张清单都是血泪教训换来的。工具层面每个工具的描述是否包含使用时机、参数是否扁平、异常是否结构化返回、是否有超时控制。循环层面最大轮数是否设置、是否有重复动作熔断、停止条件是否写进提示词。上下文层面是否有裁剪策略、关键事实是否单独保留、工具长输出是否截断。可观测层面是否记录完整轨迹、是否统计轮数与耗时、是否有失败告警。安全层面密钥是否走环境变量、工具是否有入参校验、是否限制了可访问的资源范围。这张清单不长但每一条我都见过因为漏掉而出事的案例。尤其是入参校验和资源范围限制这两条很多人觉得内部工具不用防结果模型编造出一个不存在的路径直接把整个目录读了一遍日志里全是无关内容排查了半天才发现是提示词没约束住。智能体的自由度是把双刃剑能力给得越多边界就得划得越清楚。