Agent 自定义工具开发与接入方法
1. 引言
Agent 通过工具调用(Function Call)扩展能力边界是当前主流的实现方式。标准工具库(如计算器、搜索、知识库检索等)可以覆盖通用场景,但业务系统中的定制化需求——例如查询内部工单系统、调用自研的推荐算法接口、操作公司内部的审批流程——通常需要开发自定义工具来对接。
本文说明基于 LangChain 和 Qwen-Agent 两种框架,如何完成自定义工具的完整开发与接入。内容涵盖:@tool 装饰器的使用、工具注册与参数绑定机制、安全通信与权限分级、常见踩坑与进阶技巧,以及一个从定义到集成部署的完整实战示例。读完本文后,你将能独立为现有 Agent 系统开发自定义工具,并理解底层 Function Call 协议的原理与限制。
2. 核心概念:Agent 工具调用原理与 Function Call 机制
2.1 工具调用的基本流程
Agent 调用工具的核心工作流如下:
用户输入:用户向 Agent 提交一条自然语言请求,例如“计算半径为 5 的圆面积”。
LLM 推理:LLM 根据当前对话上下文、已注册的工具列表及其描述,判断是否需要调用某个工具。如果需要,LLM 会输出一个结构化的工具调用请求,包含工具名称和参数字典。
运行时解析:Agent 框架接收 LLM 的输出,解析出工具名称与参数,查找对应的 Python 函数并执行。
结果返回:函数执行结果(字符串或结构化数据)被包装成消息传回 LLM,LLM 据此生成最终回答。
流程的闭环设计确保 Agent 可以“思考 → 行动 → 观察结果 → 再次思考”,这是 ReAct 模式的核心。
2.2 Function Call 协议与 @tool 装饰器
Function Call 是 OpenAI 在 GPT-4 系列模型中引入的一种输出格式约束:LLM 在生成回答时,如果判断需要调用工具,会输出一个 JSON 对象,包含 name 和 arguments 字段。后续推理模型(如 Claude 3、Qwen2.5)均已支持类似协议。
在 LangChain 中,@tool 装饰器是简化工具定义的标准方式。其本质是将一个普通的 Python 函数转换为一个符合 Agent 框架的 Tool 对象,自动完成以下工作:
- 提取函数签名:解析函数参数名称、类型提示(type hints)、默认值。
- 生成参数 Schema:将函数签名转换为 JSON Schema 格式(包括类型、描述、是否必需)。
- 注入文档字符串:函数的
__doc__作为工具的description字段,LLM 据此判断何时调用此工具。
因此,工具的定义质量直接决定了 LLM 能否正确调用它。
3. 自定义工具定义方式:@tool 装饰器与结构化工具类
3.1 使用 @tool 装饰器
LangChain 的 @tool 装饰器提供了最简洁的定义方式。示例:
1 | |
装饰器可以接受 name 和 description 参数来显式覆盖默认值。当函数名不足以描述其功能时,应当设置描述信息。
如果需要更精细的参数描述,可以使用 Annotated 类型:
1 | |
此时,@tool 生成的 Schema 会包含每个参数的 description,大大提升 LLM 生成正确参数的概率。
3.2 Tool 类 vs StructuredTool 类
除了装饰器,LangChain 还提供了 Tool 和 StructuredTool 两个类。
Tool类:输入为一个字符串,适用于工具只有一个输入参数的场景。例如读取文件内容、查询单值。StructuredTool类:输入为结构化参数(通过 Pydantic 模型定义),适用于多参数、参数类型丰富的场景。@tool装饰器生成的实例实际上就是StructuredTool。
建议:优先使用 @tool 装饰器,它既简洁又能自动处理参数解析。只有当需要精细控制工具的行为(如自定义错误处理、异步执行)时,才考虑手动实例化 StructuredTool。
3.3 文档字符串的重要性
LLM 依赖工具的描述信息来决定何时调用工具以及如何传参。这意味着:
- 工具描述(docstring)应当明确说明工具的功能、适用场景和边界条件。
- 参数描述应当清晰说明允许输入什么、值的范围是什么(比如数值单位、字符串编码)。
- 如果工具有副作用(如写数据库、发送邮件),务必在描述中注明。
注意:工具描述不宜过于冗长,但也绝不能为空。一个模糊的描述(如“查询信息”)会导致 LLM 在应该使用其他工具时错用此工具。
4. 工具注册与参数绑定:LangChain 与 Qwen-Agent 实践
4.1 LangChain 中的工具注册
LangChain 中注册工具的方式是将定义好的工具实例放入列表,并传递给 Agent 或 Runnable:
1 | |
AgentExecutor 负责管理工具调用的完整生命周期:接收用户输入、调用 LLM、解析工具调用、执行工具、返回结果给 LLM、生成最终回答。
在调用层面,工具对象可以直接被手动调用以验证其工作是否正常:
1 | |
4.2 Qwen-Agent 中的自定义工具注册
Qwen-Agent(阿里云的 Qwen 模型配套 Agent 框架)不依赖 LangChain,而是通过声明式 JSON Schema 来注册工具。开发者需要提供一个 functions 列表,每个元素包含 name、description、parameters 等字段。
典型的注册流程:
1 | |
Qwen-Agent 还提供了 FunctionPlugin 工具类,允许开发者以类方式实现工具的生命周期管理(初始化、执行、清理),适用于状态复杂的工具。
4.3 两框架对比
| 维度 | LangChain | Qwen-Agent |
|---|---|---|
| 工具定义方式 | @tool 装饰器 / StructuredTool |
JSON Schema / FunctionPlugin 类 |
| 参数解析 | 自动根据函数签名与类型提示生成 Schema | 手动编写 Schema(需要与函数签名保持一致) |
| 推荐适用场景 | 快速原型、多框架混用 | 纯 Qwen 模型、阿里云生态内的项目 |
| 学习成本 | 较低(装饰器模式直观) | 较高(需理解 JSON Schema 规范) |
底层均依赖 Function Call 协议,两种框架可以互换工具定义,但需要在集成时做一层适配。
5. 安全通信与权限分级:工具白名单、用户确认与前端集成
工具一旦暴露给 Agent,Agent 就有可能执行具有副作用的操作(如删除文件、发送消息、修改数据库)。生产环境中,必须建立安全机制。
5.1 工具安全的三层设计
第一层:前端工具白名单
前端(客户端)只允许执行服务端声明的工具。具体做法是:服务端在初始化 Agent 时,维护一个工具 ID 列表;前端通过 SSE 或 WebSocket 接收到工具调用指令时,先验证工具 ID 是否在当前会话的白名单内,不在白名单中的调用直接拒绝。
第二层:HTTPS + 签名防篡改
在前后端之间传输的工具调用指令应当经过签名(如 HMAC-SHA256),确保指令在传输途中未被篡改。若指令非法,前端应拒绝执行并上报服务端。
第三层:用户二次确认
对于高风险工具(如写数据库、发送消息、删除资源),需要在工具元数据中标注权限级别。前端在接收此类工具调用指令时,弹出确认对话框,获得用户显式同意后才执行。
5.2 网页端嵌入 Agent 的前端集成经验
在“网页端嵌入 Agent 前端方案”的经验中,服务端负责 LLM 推理与工具目录维护,前端负责工具执行与界面呈现。关键实现点:
- 服务端通过 SSE 流式向下推送消息(包括工具调用指令)。
- 前端收到工具调用消息后,先解析指令内容,校验工具 ID 是否在白名单内。
- 若校验通过,调用前端处实现的具体工具函数(如
showConfirmDialog、updateDom);若校验失败,向前端提示错误。 - 结果通过 SSE 流或 HTTP 回调返回服务端。
5.3 权限分级示例
在工具元数据中增加 permission 字段:
1 | |
前端根据 permission 字段判断是否需要弹窗确认。建议的分级规则:
read_only:无副作用,自动执行。write:有副作用,必须用户二次确认。admin:涉及系统级操作,除确认外还需管理员密码。
6. 进阶技巧与常见踩坑
6.1 工具执行超时与幂等性
Agent 调用工具的默认行为是同步等待。如果工具执行耗时过长(如查询外部 API 超时),会导致 Agent 响应周期拉长,用户体验下降。
建议做法:
- 为工具设置超时时间(如 30s),LangChain 中可通过
asyncio.wait_for或ThreadPoolExecutor实现。 - 工具函数尽量设计为幂等:相同的输入重复执行多次,结果一致且无副作用。这是避免错误回调或重试时产生数据污染的关键。
6.2 上下文隔离与多 Agent 协同
在多 Agent 系统中,不同 Agent 可能会调用相同的工具。如果工具内部持有状态(如缓存、计数器),就会产生串扰。
解决方案:
- 使用
agentId作为 Key 来隔离工具调用上下文。例如在工具函数内通过context.get("agentId")判断当前调用所属的 Agent。 - 使用
ChatMemory管理工具调用的历史记录,确保每个 Agent 只能访问自己的历史。
6.3 参数解析失败的处理
LLM 生成的工具调用参数并非总是符合预期。常见问题包括:参数类型错误(传了字符串却期望数字)、缺少必需参数、参数值为 null。
处理策略:
- 在工具函数入口处做类型校验和默认值回退。
- LangChain 的
ToolException机制允许工具抛出一个特殊异常,框架将异常信息返回给 LLM 重试。 - 记录失败案例,用于后续优化 Agent 的 Prompt 或工具描述。
6.4 前端性能优化
当 Agent 频繁执行 DOM 操作的工具(如实时修改页面布局、插入元素)时,直接操作 DOM 会导致大量重排重绘。
推荐做法:使用离屏 DOM(DocumentFragment)进行批量修改,修改完成后再一次性交换到主 DOM 中。可以将工具调用聚合并批量处理,而不是逐条执行。
7. 实战示例:开发一个内部知识库查询工具并集成
7.1 场景定义
业务需求:用户可以通过 Agent 查询公司内部的 Wiki 知识库,获取相关文档摘要。工具需要接收 query 和 top_k 参数。
7.2 LangChain 实现
1 | |
7.3 Qwen-Agent 等价实现
1 | |
7.4 效果验证
执行 executor.invoke 后,正常情况下打印的输出应当为:
1 | |
若 Agent 未调用工具(直接 LLM 生成回答),检查工具描述是否清晰、Prompt 是否明确要求调用工具。
8. 总结与拓展
核心回顾
Agent 自定义工具开发与接入的关键步骤:
定义工具:使用
@tool装饰器(LangChain)或手动编写 JSON Schema(Qwen-Agent),确保描述清晰、参数类型正确。注册绑定:将工具实例列表传入 Agent 或使用
functions参数注册到 LLM。安全控制:通过前端白名单、签名防篡改、用户二次确认三层保障的工具安全。
前后端集成:服务端维护工具目录,前端通过 SSE 接收指令并校验权限,执行后反馈结果。
拓展方向
- SSE 流式对接前端:在工具调用过程中向前端实时推送状态更新(如“正在调用工具A,参数为…”),提升用户体验。
- WebAssembly 客户端运行轻量 LLM:在客户端用小模型(如 TinyLlama)做部分工具决策,减少服务端压力,支持离线场景。
- 多 Agent 间通过 A2A 协议相互调用工具:一个 Agent 的工具可以被另一个 Agent 的
send_task调用,实现任务分解与协作自动化。
建议持续关注 LangChain 和 Qwen-Agent 框架的官方更新,并将本文方案作为团队自定义工具开发的初始参考,结合实际业务场景进行适配与扩展。
总结
通过本文的学习,相信你已经对「LangChain」有了更深入的理解。建议结合实际项目多加练习。如有疑问,欢迎交流!