Agent 自定义工具开发与接入方法

1. 引言

Agent 通过工具调用(Function Call)扩展能力边界是当前主流的实现方式。标准工具库(如计算器、搜索、知识库检索等)可以覆盖通用场景,但业务系统中的定制化需求——例如查询内部工单系统、调用自研的推荐算法接口、操作公司内部的审批流程——通常需要开发自定义工具来对接。

本文说明基于 LangChain 和 Qwen-Agent 两种框架,如何完成自定义工具的完整开发与接入。内容涵盖:@tool 装饰器的使用、工具注册与参数绑定机制、安全通信与权限分级、常见踩坑与进阶技巧,以及一个从定义到集成部署的完整实战示例。读完本文后,你将能独立为现有 Agent 系统开发自定义工具,并理解底层 Function Call 协议的原理与限制。

2. 核心概念:Agent 工具调用原理与 Function Call 机制

2.1 工具调用的基本流程

Agent 调用工具的核心工作流如下:

  1. 用户输入:用户向 Agent 提交一条自然语言请求,例如“计算半径为 5 的圆面积”。

  2. LLM 推理:LLM 根据当前对话上下文、已注册的工具列表及其描述,判断是否需要调用某个工具。如果需要,LLM 会输出一个结构化的工具调用请求,包含工具名称和参数字典。

  3. 运行时解析:Agent 框架接收 LLM 的输出,解析出工具名称与参数,查找对应的 Python 函数并执行。

  4. 结果返回:函数执行结果(字符串或结构化数据)被包装成消息传回 LLM,LLM 据此生成最终回答。

流程的闭环设计确保 Agent 可以“思考 → 行动 → 观察结果 → 再次思考”,这是 ReAct 模式的核心。

2.2 Function Call 协议与 @tool 装饰器

Function Call 是 OpenAI 在 GPT-4 系列模型中引入的一种输出格式约束:LLM 在生成回答时,如果判断需要调用工具,会输出一个 JSON 对象,包含 namearguments 字段。后续推理模型(如 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
2
3
4
5
6
from langchain.tools import tool

@tool
def circle_area(radius: float) -> str:
"""计算圆的面积。参数 radius: 圆的半径。"""
return f"半径为 {radius} 的圆面积为 {3.14159 * radius * radius:.2f}"

装饰器可以接受 namedescription 参数来显式覆盖默认值。当函数名不足以描述其功能时,应当设置描述信息。

如果需要更精细的参数描述,可以使用 Annotated 类型:

1
2
3
4
5
6
7
8
9
from typing import Annotated

@tool
def search_articles(
query: Annotated[str, "搜索关键词,支持模糊匹配"],
top_k: Annotated[int, "返回结果条数,默认 3"] = 3
) -> str:
"""在内部知识库中搜索相关文档。"""
# 实现略

此时,@tool 生成的 Schema 会包含每个参数的 description,大大提升 LLM 生成正确参数的概率。

3.2 Tool 类 vs StructuredTool

除了装饰器,LangChain 还提供了 ToolStructuredTool 两个类。

  • Tool:输入为一个字符串,适用于工具只有一个输入参数的场景。例如读取文件内容、查询单值。
  • StructuredTool:输入为结构化参数(通过 Pydantic 模型定义),适用于多参数、参数类型丰富的场景。@tool 装饰器生成的实例实际上就是 StructuredTool

建议:优先使用 @tool 装饰器,它既简洁又能自动处理参数解析。只有当需要精细控制工具的行为(如自定义错误处理、异步执行)时,才考虑手动实例化 StructuredTool

3.3 文档字符串的重要性

LLM 依赖工具的描述信息来决定何时调用工具以及如何传参。这意味着:

  • 工具描述(docstring)应当明确说明工具的功能、适用场景和边界条件。
  • 参数描述应当清晰说明允许输入什么、值的范围是什么(比如数值单位、字符串编码)。
  • 如果工具有副作用(如写数据库、发送邮件),务必在描述中注明。

注意:工具描述不宜过于冗长,但也绝不能为空。一个模糊的描述(如“查询信息”)会导致 LLM 在应该使用其他工具时错用此工具。

4. 工具注册与参数绑定:LangChain 与 Qwen-Agent 实践

4.1 LangChain 中的工具注册

LangChain 中注册工具的方式是将定义好的工具实例放入列表,并传递给 Agent 或 Runnable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain.agents import create_openai_functions_agent, AgentExecutor
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [circle_area, search_articles]

prompt = ChatPromptTemplate.from_messages([
("system", "你是一个有帮助的助手,可以使用提供的工具。"),
("user", "{input}"),
])

agent = create_openai_functions_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)

AgentExecutor 负责管理工具调用的完整生命周期:接收用户输入、调用 LLM、解析工具调用、执行工具、返回结果给 LLM、生成最终回答。

在调用层面,工具对象可以直接被手动调用以验证其工作是否正常:

1
circle_area.invoke({"radius": 5})  # 返回 "半径为 5 的圆面积为 78.54"

4.2 Qwen-Agent 中的自定义工具注册

Qwen-Agent(阿里云的 Qwen 模型配套 Agent 框架)不依赖 LangChain,而是通过声明式 JSON Schema 来注册工具。开发者需要提供一个 functions 列表,每个元素包含 namedescriptionparameters 等字段。

典型的注册流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义工具函数
def search_articles(query: str, top_k: int = 3) -> str:
"""在内部知识库中搜索相关文档。"""
# 实现略

# 定义对应的 JSON Schema
search_articles_schema = {
"name": "search_articles",
"description": "在内部知识库中搜索相关文档",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"top_k": {"type": "integer", "description": "返回结果条数", "default": 3}
},
"required": ["query"]
}
}

# 注册到 Agent
# 使用 Agent.run 时传入 functions=search_articles_schema
# 当 LLM 决定调用此工具时,框架会回调 search_articles 函数

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 是否在白名单内。
  • 若校验通过,调用前端处实现的具体工具函数(如 showConfirmDialogupdateDom);若校验失败,向前端提示错误。
  • 结果通过 SSE 流或 HTTP 回调返回服务端。

5.3 权限分级示例

在工具元数据中增加 permission 字段:

1
2
3
4
5
6
7
8
9
from langchain.tools import tool

@tool
def delete_user(user_id: str) -> str:
"""删除指定用户(高风险操作,需二次确认)。"""
# 实现略

# 在元数据中标注权限
delete_user.permission = "write"

前端根据 permission 字段判断是否需要弹窗确认。建议的分级规则:

  • read_only:无副作用,自动执行。
  • write:有副作用,必须用户二次确认。
  • admin:涉及系统级操作,除确认外还需管理员密码。

6. 进阶技巧与常见踩坑

6.1 工具执行超时与幂等性

Agent 调用工具的默认行为是同步等待。如果工具执行耗时过长(如查询外部 API 超时),会导致 Agent 响应周期拉长,用户体验下降。

建议做法:

  • 为工具设置超时时间(如 30s),LangChain 中可通过 asyncio.wait_forThreadPoolExecutor 实现。
  • 工具函数尽量设计为幂等:相同的输入重复执行多次,结果一致且无副作用。这是避免错误回调或重试时产生数据污染的关键。

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 知识库,获取相关文档摘要。工具需要接收 querytop_k 参数。

7.2 LangChain 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from langchain.tools import tool
from langchain.agents import create_react_agent, AgentExecutor
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

# 模拟知识库检索函数
def retrieve_from_vector_db(query: str, top_k: int) -> list:
"""调用向量数据库检索相关文档。此处做简化实现。"""
# 实际项目中使用 embedding_model.embed_query(query)
# 然后连接向量存储查询
return [{"title": f"文档 {i}", "summary": f"与 '{query}' 相关的摘要"} for i in range(top_k)]

@tool
def query_knowledge_base(
query: Annotated[str, "用户搜索的内容,支持中文"],
top_k: Annotated[int, "返回结果的数量,默认3"] = 3
) -> str:
"""在内部知识库中查询相关文档并返回摘要列表。

"""
results = retrieve_from_vector_db(query, top_k)
if not results:
return "未找到相关文档。"
formatted = "\n".join([f"- {r['title']}: {r['summary']}" for r in results])
return f"找到以下相关文档:\n{formatted}"

# 注册到 Agent
tools = [query_knowledge_base]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

prompt = ChatPromptTemplate.from_messages([
("system", "你是一个内部知识库助手,请使用 query_knowledge_base 工具回答用户问题。"),
("user", "{input}"),
])

agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)

# 执行测试
result = executor.invoke({"input": "帮我查找关于项目A的文档,返回最相关的2条"})
print(result["output"])

7.3 Qwen-Agent 等价实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义工具函数(同 LangChain 版本)
def query_knowledge_base(query: str, top_k: int = 3) -> str:
results = retrieve_from_vector_db(query, top_k)
return ... # 格式化返回

# 定义 Schema
query_kb_schema = {
"name": "query_knowledge_base",
"description": "在内部知识库中查询相关文档并返回摘要列表",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "用户搜索的内容"},
"top_k": {"type": "integer", "description": "返回结果的数量", "default": 3}
},
"required": ["query"]
}
}

# Agent 调用(伪代码)
# agent = Agent(...)
# response = agent.run("帮我查找关于项目A的文档", functions=[query_kb_schema])

7.4 效果验证

执行 executor.invoke 后,正常情况下打印的输出应当为:

1
2
3
找到以下相关文档:
- 文档 1: 与 '项目A 文档' 相关的摘要
- 文档 2: 与 '项目A 文档' 相关的摘要

若 Agent 未调用工具(直接 LLM 生成回答),检查工具描述是否清晰、Prompt 是否明确要求调用工具。

8. 总结与拓展

核心回顾

Agent 自定义工具开发与接入的关键步骤:

  1. 定义工具:使用 @tool 装饰器(LangChain)或手动编写 JSON Schema(Qwen-Agent),确保描述清晰、参数类型正确。

  2. 注册绑定:将工具实例列表传入 Agent 或使用 functions 参数注册到 LLM。

  3. 安全控制:通过前端白名单、签名防篡改、用户二次确认三层保障的工具安全。

  4. 前后端集成:服务端维护工具目录,前端通过 SSE 接收指令并校验权限,执行后反馈结果。

拓展方向

  • SSE 流式对接前端:在工具调用过程中向前端实时推送状态更新(如“正在调用工具A,参数为…”),提升用户体验。
  • WebAssembly 客户端运行轻量 LLM:在客户端用小模型(如 TinyLlama)做部分工具决策,减少服务端压力,支持离线场景。
  • 多 Agent 间通过 A2A 协议相互调用工具:一个 Agent 的工具可以被另一个 Agent 的 send_task 调用,实现任务分解与协作自动化。

建议持续关注 LangChain 和 Qwen-Agent 框架的官方更新,并将本文方案作为团队自定义工具开发的初始参考,结合实际业务场景进行适配与扩展。

总结

通过本文的学习,相信你已经对「LangChain」有了更深入的理解。建议结合实际项目多加练习。如有疑问,欢迎交流!