尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

大模型Agent扩展实践:工具调用、模型适配与多Agent协作

大模型Agent扩展实践:工具调用、模型适配与多Agent协作 前段时间整理7.2版本的项目笔记把 HelloAgentsLLM 又翻出来过了一遍。这个项目说白了就是一个基于大语言模型的 Agent 示例框架核心思路是让模型不再只是“聊天”而是能调用外部工具、读取外部数据、按流程完成任务。7.2 这版我做的主要工作就一个词——扩展。很多人搜“扩展”会搜到浏览器扩展、HEVC 视频扩展、磁盘分区扩展这些完全不相干的东西但在 LLM 开发这个圈子里扩展指的是给 Agent 框架新增能力、接入新工具、适配新模型甚至把单一 Agent 变成多 Agent 协作系统。这个版本我把整个扩展链路理顺了从工具注册到模型适配从上下文管理到错误排查都有一套可复用的操作流程。这篇文章适合三类人一类是刚接触 Agent 开发、想知道怎么给现有框架加功能的新手一类是已经在写 LLM 应用、但每次扩展都靠硬编码、改到怀疑人生的中级开发者还有一类是想把多个 Agent 串起来做自动化流程的进阶玩家。我会从设计思路、核心扩展点、完整实操、常见问题四个方向展开把我在 7.2 版本里实际踩过的坑和验证过的方法都写出来保证每个步骤都能直接复现。1. 从“能聊天”到“能干活”HelloAgentsLLM 为什么必须做扩展1.1 单体 Prompt 的天花板刚开始接触大语言模型的人都会有个错觉只要把 Prompt 写得足够长、足够细模型什么都能干。这句话在简单场景下成立但一旦涉及真实业务马上就会露馅。比如你想让 Agent 帮你查数据库里的订单数据、调用内部接口发消息、读取本地文件生成报表这些操作靠 Prompt 是做不到的——模型本身没有执行能力它只能“说”不能“做”。这正是 HelloAgents 这类框架存在的意义把模型的决策能力和外部工具的执行能力粘合在一起。而 7.2 版本之前的实现本质上还是一个单体结构一个 Agent、一套固定工具、一条 Prompt 模板。想加一个新工具得改主程序想换一个模型得改调用层想加一段业务逻辑得改核心代码。改到最后代码里全是 if else新的需求一来就牵一发动全身。所以 7.2 版本的第一优先级不是加更多功能而是先把扩展能力做出来让这个框架可以“接得住”后续所有的变化。1.2 扩展的三个维度能力、模型、流程我做扩展设计时把“扩展”这件事拆成了三个独立的维度每个维度互不干扰可以单独演进。第一个是能力扩展。Agent 能干什么取决于它注册了哪些工具。7.2 里我实现了统一的工具注册机制任何新增能力都只需要写一个标准的工具函数然后在配置里声明一下就能被 Agent 自动发现和调用。这个机制有点类似插头插座的关系工具是插头框架是插座标准统一了什么电器都能插上去。第二个是模型扩展。不同任务的性价比差异很大复杂推理任务用 GPT 级别的模型简单分类任务用轻量模型本地私有化部署还需要接开源模型。7.2 做了一个模型适配层统一了不同厂商 API 的调用格式切换模型时不需要改业务代码只需要改一个配置项。第三个是流程扩展。单 Agent 的能力再强也有边界多 Agent 协作才是解决复杂问题的正路。7.2 里我加入了简单的 Agent 间消息传递机制两个 Agent 可以通过共享上下文协作一个负责拆解任务一个负责执行任务后续还可以扩展出更多角色。这三个维度也是我在前几次版本迭代里反复被需求逼着走之后总结出来的。一开始我图省事把所有扩展都堆在一个文件里结果每加一个功能都要重新读一遍全部代码头大。拆开之后每个维度的改动都可以独立测试、独立回滚出问题也容易定位。1.3 配置驱动还是代码驱动扩展设计的第一道选择题做扩展设计时还有一个关键取舍新增能力的方式是用配置文件声明还是在代码里硬编码。我见过不少项目选了后者原因是图方便但代价是每次加工具都要发版、重启服务而且新人接手时根本不知道哪个函数被哪里调用了。7.2 的结论是工具注册用配置驱动逻辑复用用代码驱动。也就是说一个工具函数写好后它在什么场景下被调用、需要传什么参数、返回什么结果这些元信息全部放在一个配置表里。Agent 运行时先读配置再动态加载对应的工具模块。这样加一个新工具不需要修改框架源码只需要新增一个工具文件外加一条配置记录重启之后立即可用。这个设计在第一版实现时确实要费点功夫因为动态加载、参数校验、错误处理都要做成通用逻辑。但从项目长远维护的角度看这步投资非常值得。7.2 版本之后我再加工具平均耗时不超过十分钟而且几乎没有因为新工具引入而破坏旧功能的情况。2. 工具调用扩展核心机制与实现细节2.1 Function Calling 的原理模型如何“知道”有这些工具工具扩展是 HelloAgentsLLM 最核心的能力而它底层依赖的是大模型 API 提供的 Function Calling函数调用机制。理解了这个机制你才能把工具扩展做好。简单说Function Calling 是这样一个过程你把工具的描述信息包括工具名、功能说明、参数结构以 JSON Schema 的形式传给模型 API模型根据用户的提问和这些工具描述决定要不要调用某个工具、参数填什么然后返回一个结构化的“调用请求”。你的程序收到这个请求后真正去执行对应的函数再把执行结果回传给模型让模型基于结果生成最终的回复。这个过程本质上就像你把一份“服务菜单”递给了一个超级聪明的店员。店员看了客人的需求告诉你该上哪道菜、需要什么配料但真正下厨的还得是你。工具扩展要做的就是把新菜的菜名、配料、做法写进这份菜单里。2.2 工具接口规范参数描述决定调用成功率在 7.2 里我定义了一个标准的工具接口所有工具函数都遵循同一个签名模式。这里直接放出模板代码from typing import Any, Dict def my_tool(param1: str, param2: int 0) - Dict[str, Any]: 工具函数描述这个工具干什么 Args: param1: 参数1的说明 param2: 参数2的说明 Returns: Dict: 包含执行结果的字典 # 在这里写你的业务逻辑 result {status: success, data: } return result这个模板有几个关键点。第一函数的 docstring 要写清楚用途和参数说明因为模型会读取这些描述来决定是否调用。第二尽可能用类型注解这是给参数校验用的。第三返回值必须是一个字典至少包含一个键值对方便统一处理错误。接下来是注册信息。在 7.2 中我使用了一个 JSON 文件来管理工具清单格式如下[ { name: my_tool, description: 这个工具用来做什么, parameters: { type: object, properties: { param1: { type: string, description: 参数1的说明 }, param2: { type: integer, description: 参数2的说明 } }, required: [param1] } } ]注意这里的参数描述要尽量详细尤其是枚举值一定要写明有哪些可选。否则模型大概率会给你编一个不存在的值出来。我一个血泪教训是有个工具的参数设计成model_type取值范围是fast和accurate但描述里只写了“模型类型”没写可选值结果模型返回了个gpt-4o接口直接报错。2.3 动态加载与参数映射工具函数写好了配置表也维护好了接下来就是框架如何把配置变成可调用的函数。7.2 里我用 Python 的importlib做动态导入实现逻辑不复杂import importlib def load_tool(tool_name: str): 根据工具名称加载工具模块返回可调用对象 module_path ftools.{tool_name} try: module importlib.import_module(module_path) func getattr(module, tool_name) return func except (ImportError, AttributeError) as e: raise RuntimeError(f工具 {tool_name} 加载失败: {e})参数映射是另一个容易出问题的点。模型返回的参数可能是 JSON 字符串需要解析成 Python 字典然后通过**kwargs传给工具函数。这一层一定要做严格的类型检查我之前吃过亏模型返回的数值参数是字符串类型工具函数里直接做了算术运算结果炸了。所以 7.2 里我在调用前强制校验参数类型不匹配就抛异常并反馈给模型重新生成。2.4 工具执行结果回传与错误处理工具执行完之后结果要回传给模型。这一步有个关键细节回传给模型的结果不一定是给用户看的完整数据。模型上下文窗口有限尤其是免费或低配模型上下文太长了不仅费钱还会影响响应速度。所以我通常在工具函数内部就把结果整理成精简的文本摘要只保留关键信息。错误处理也不能马虎。工具调用失败是常态可能是网络超时、接口限流、参数不合法。7.2 里我统一约定工具函数执行失败时必须返回一个包含error键的字典而不是直接抛异常。这样框架层可以捕获这个错误信息连同提示语一起回传给模型让模型知道工具没调通可以尝试换一种方式或向用户说明情况。如果直接抛异常整个对话流程就断了用户体验很糟糕。3. 模型适配层一套代码接多家模型3.1 为什么要做模型适配层HelloAgentsLLM 最早的版本只支持单一厂商的 API后来实际使用时发现不切实际。企业客户要求私有化部署要接本地开源模型个人开发者想把不同任务分流到不同模型上省钱还有的项目需要在一个流程里“思维链”任务用强模型、简单任务用快模型。如果每次切换模型都去改业务层的代码那项目基本没法维护。所以 7.2 里我加入了一个模型适配层核心是一个统一的调用接口class BaseLLMClient: def chat(self, messages: list, tools: list None, **kwargs) - dict: 统一聊天接口tools 为工具定义列表 raise NotImplementedError def parse_response(self, response: dict) - dict: 解析模型响应提取文本或工具调用请求 raise NotImplementedError所有模型客户端都继承这个基类实现各自的chat和parse_response方法。业务层只依赖这个抽象接口不关心底层到底是哪家模型。3.2 不同模型 API 的差异处理做这套适配层时最头疼的是一些细小的差异。比如有的模型把工具调用放在tool_calls字段里有的放在function_call里有的模型返回的是归一的 JSON有的返回的是 Markdown 代码块包的 JSON有的甚至会在 JSON 之外混入自然语言描述。我在parse_response里做了兼容处理核心思路是先把响应文本提取出来然后做多层解析尝试import json import re def extract_json(text: str) - dict: 从模型返回文本中提取 JSON 对象 if not text: raise ValueError(空响应) # 移除代码块标记 text re.sub(r(?:json)?, , text).strip().rstrip() try: return json.loads(text) except json.JSONDecodeError: pass # 尝试提取 {} 对 match re.search(r\{.*\}, text, re.S) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass raise ValueError(f无法从文本中解析 JSON: {text[:200]})这套解析逻辑实测下来能覆盖绝大多数模型的返回格式。只有极少数模型返回的 JSON 嵌套特别深正则提取会截断这种情况我建议直接用该模型官方 SDK 提供的解析能力不要走通用路径。3.3 配置驱动的模型切换模型切换在 7.2 里做成纯配置操作比如config.yaml里这样配置models: default: provider: openai model_name: gpt-4o-mini api_key_env: OPENAI_API_KEY fast: provider: anthropic model_name: claude-3-haiku api_key_env: ANTHROPIC_API_KEY业务代码里使用模型时通过一个get_model(name)工厂方法获取实例。这样切换模型就是改一行配置的事不需要碰代码。要注意的是不同的模型在 Function Calling 上的能力差异非常大。我实测下来目前主流大模型对工具调用的支持都已经比较好了但一些轻量版或开源版本可能不支持工具调用或者只能传递少量工具定义。碰到这种情况适配层要能降级处理不支持工具调用的模型就退回“把工具说明放进系统提示词让模型按格式输出指令”的旧方案。这个降级逻辑我在 7.2 里单独封装了一个模块后续换新模型时可以直接复用。4. 完整实操从零扩展一个天气查询工具4.1 环境准备与项目结构为了让步骤可复现我先说一下环境。我的运行环境是 Python 3.10安装了openai、anthropic、pyyaml这几个基础依赖。项目结构按功能拆成下面这样helloagents/ ├── config.yaml # 全局配置 ├── main.py # 入口 ├── agents/ │ ├── base_agent.py # Agent 基类 │ └── react_agent.py # ReAct 风格 Agent ├── clients/ │ ├── base_client.py # 模型客户端基类 │ ├── openai_client.py │ └── anthropic_client.py ├── tools/ │ └── __init__.py └── tool_registry.json # 工具清单这个结构其实就体现了前面说的扩展思路新增工具放tools/注册到tool_registry.json模型客户端放在clients/业务逻辑在agents/里编排互不干扰。4.2 编写天气工具函数我们现在来扩展一个天气查询工具。假设我们要对接一个第三方的天气 API工具函数这样写# tools/get_weather.py from typing import Any, Dict import requests def get_weather(city: str, date: str ) - Dict[str, Any]: 查询指定城市的天气信息 Args: city: 城市名称例如北京、上海 date: 日期格式 YYYY-MM-DD默认当天 Returns: Dict: 包含天气信息的字典 # 实际操作中用 requests 调用天气服务商接口 url https://api.example.com/weather params {city: city, date: date} try: resp requests.get(url, paramsparams, timeout5) resp.raise_for_status() data resp.json() summary ( f{city} {date or 今天}天气{data[condition]} f温度 {data[low]}~{data[high]}°C f湿度 {data[humidity]}%风力 {data[wind]} ) return {status: success, summary: summary, data: data} except Exception as e: return {error: f天气查询失败: {str(e)}}这里有一个实操技巧返回结果里我专门加了一个summary字段这是给模型回看用的精简文本不要把原始 JSON 全量回传否则上下文会被无关字段大量占用。如果模型需要对天气做进一步分析可以再让模型在后续对话中追问详情。4.3 注册工具并验证调用下一步是在tool_registry.json里登记这个工具[ { name: get_weather, description: 查询城市天气适合回答关于天气、温度、湿度、风力等问题, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海、广州 }, date: { type: string, description: 日期格式 YYYY-MM-DD不填则默认当天 } }, required: [city] } } ]然后重启服务在测试脚本里发起一个包含工具调用意图的请求from agents.react_agent import ReActAgent from clients.openai_client import OpenAIClient client OpenAIClient.from_config(config.yaml) agent ReActAgent(clientclient, tool_registrytool_registry.json) response agent.run(北京今天天气怎么样需不需要穿外套) print(response)我实测下来的输出大概是这样的北京今天2025-XX-XX天气晴温度 18~26°C湿度 40%风力 3级。 白天体感温度舒适建议穿薄长袖或短袖加薄外套早晚略凉出门可带一件外套。模型能正确触发天气工具、拿到结果后给出建议说明整条工具调用链路已经通了。从新增代码到实际可用整个流程不到十分钟。4.4 扩展到多 Agent 协作场景单个天气工具只能解决单一问题但实际业务经常需要多个 Agent 配合。7.2 里我试验了一个场景一个“行程规划 Agent”负责拆解用户需求一个“天气 Agent”负责查天气一个“交通 Agent”负责查交通信息。行程规划 Agent 收到“北京一日游”的需求后自动把任务拆成“查天气”“查交通”“生成行程”三个子任务分别调用对应 Agent汇总结果后输出。实现方式是给 Agent 增加一个子 Agent 调用工具本质上和普通工具一样只是执行体从“函数”变成“另一个 Agent”def call_sub_agent(agent_name: str, task: str) - dict: 调用另一个 Agent 执行任务 sub_agent get_agent(agent_name) result sub_agent.run(task) return {status: success, summary: result}这种设计的好处是Agent 之间通过消息传递协作互不感知对方内部实现。新增一个 Agent 角色时不影响已有 Agent 的稳定运行。缺点也有就是上下文消耗变高、响应延迟变大编排复杂任务时需要控制好子任务拆分的粒度避免任务太小导致反复调用的开销超过收益。5. 常见问题与排查技巧实录5.1 工具调用没触发现象用户问了“北京天气”模型直接说“我无法实时查询天气”没有调用工具。排查思路第一先确认tool_registry.json格式是否正确、工具描述是否准确。模型只有在认为现有工具能解决用户问题时才会触发调用描述写得不清晰例如只写了“查天气”没写“可以查询指定城市温度、湿度、风力”模型可能判断为能力不足。第二检查模型是否启用了 tools 参数很多 API 默认不传 tools 就不会启用函数调用。第三看模型的 max_tokens 是否设置过小有些模型在工具调用和生成文本之间冲突时优先截断工具调用。我遇到的一次典型情况是max_tokens100模型刚输出工具调用的开头就被截断了表现为“模型在说废话没有真正发起调用”。调大到 512 之后恢复正常。5.2 模型返回的工具名或参数不存在现象模型返回了一个get_weather的调用请求但参数里有temperature_unit这个字段工具函数没有这个参数直接报 TypeError。这类问题要从两头堵。一头是在工具描述里把参数的枚举值、默认值写清楚减少模型“自由发挥”的空间。另一头是在框架层做参数过滤def safe_call(func, params: dict): import inspect sig inspect.signature(func) valid_keys set(sig.parameters.keys()) filtered {k: v for k, v in params.items() if k in valid_keys} return func(**filtered)如果过滤后必填参数缺失就不要硬调把错误信息返回给模型让它补充参数重新发起调用。5.3 上下文长度超限现象多轮对话后突然报错提示上下文超过模型上限。我在做多 Agent 协作时最先踩到这个坑。每个 Agent 把自己的完整对话历史传给下一个 Agent几轮下来上下文直接爆掉。解决办法是引入“摘要压缩”每个 Agent 只对外传递最终结果摘要不传递内部对话历史。另外在 Agent 基类里实现一个简单的上下文裁剪逻辑当对话超过阈值时把早期的消息压缩成一条系统摘要。def compress_context(self, messages: list, max_messages: int 20): if len(messages) max_messages: return messages head messages[:2] # 系统提示词 tail messages[-max_messages 2:] summary self._summarize(messages[2:-max_messages2]) return head [{role: assistant, content: f历史对话摘要{summary}}] tail这个方法不算完美因为摘要会丢失细节但在上下文有限的情况下是最实用的兜底手段。如果任务对历史细节敏感建议优先考虑扩充上下文窗口或换更长的模型而不是无脑压缩。5.4 API 限流与并发控制现象工具调用频繁或者并发请求一多API 返回 429 限流错误。Function Calling 场景下这个问题尤其突出因为一次用户提问往往要经历“模型返回工具调用请求 → 执行工具 → 结果回传模型 → 模型最终回复”两轮 API 请求消耗和延迟都是双倍的。7.2 里我做了两个层面的优化一是给 API 请求加超时和重试机制指数退避重试三次二是在工具并发调用时限制并发数实测并发控制在 5 以内比较稳妥。如果业务要求高吞吐建议直接购买更高 tier 的 API 配额而不是在代码层死磕。下面整理一个速查表方便排查现象可能原因处理建议模型没有调用工具工具描述不清晰、未传 tools 参数、max_tokens 过小完善工具描述检查请求参数调大 max_tokens工具参数报错模型返回了未定义的参数描述中写清枚举值框架层做参数过滤上下文超限多轮对话累积、多 Agent 互传完整历史实现上下文压缩只传结果摘要限流 429API 配额不足、并发过高指数退避重试控制并发数JSON 解析失败模型返回了带污染的 JSON多层解析策略优先用官方 SDKAgent 间循环调用子 Agent 任务拆解不合理设置最大递归深度拆解任务前检查是否原子5.5 一个容易被忽略的小问题工具结果回传格式最后说一个小但坑人的细节。工具执行结果回传给模型时不同模型 API 要求的格式不一样。OpenAI 允许直接传字符串内容但有些模型要求必须是特定结构体里面要有content字段。7.2 的适配层里我在每个模型客户端内部单独处理这个格式转换避免业务层混乱。这也是为什么我强烈建议一定要在框架层做模型适配而不是在每个工具函数里直接调 API——否则光是格式适配就能让你改到崩溃。写在最后一些实际体会我做完 7.2 这轮扩展之后最大的感受是扩展能力的核心不是“加功能”而是“定标准”。工具接口统一了模型适配层统一了配置格式统一了后面所有扩展都变成填空题而不是叙述题。现在新加一个工具我基本不碰框架代码把函数写好、配置写好测试一下就能上线。如果你正在做一个类似的项目我的建议是先做一个最小闭环——让一个工具从注册、调用、回传到最终回复完整跑通然后再往里面加花样。不要一上来就想把所有模型厂商都适配了先固定一家把链路打通再抽象适配层。另外多读你所用模型官方文档里关于 Function Calling 的详细说明不同版本之间行为差异很大文档是最可靠的依据。后续我打算在这个项目里继续扩展的方向有两个一是给 Agent 加入短期记忆和长期记忆的分层管理让多轮对话更自然二是做一个可视化的 Agent 编排画布把任务拆解、工具调用、结果汇总这些流程拖拽化进一步降低使用门槛。如果你也在玩 Agent 扩展欢迎在评论区交流你的踩坑经验。
返回列表