ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第8篇:Agent工具系统 —— 从Function Calling到自定义Tool

第8篇:Agent工具系统 —— 从Function Calling到自定义Tool 第8篇Agent工具系统 —— 从Function Calling到自定义Tool工具Tool是Agent与外部世界交互的接口。没有工具的Agent只是一个“对话模型”有了工具的Agent才能真正“行动”——搜索信息、查询数据库、发送邮件、执行代码。本文系统讲解工具调用的完整链路从JSON Schema的工具定义、模型推理返回tool_calls、到应用程序执行函数并回传结果的多轮闭环。以博查搜索API为例展示如何为Agent集成自定义搜索工具并实现“模拟搜索”作为降级方案。一、为什么工具是Agent的“手脚”大语言模型本质上是“大脑”——它擅长推理、规划、生成文本但无法主动获取外部信息或执行具体操作。当用户问“今天北京天气怎么样”时模型要么基于过时的训练数据编造答案要么诚实地说“我不知道”。这就是模型的“能力边界”。工具系统的作用就是让模型能够调用外部函数从而突破这一边界。工具调用的核心流程是一个三方的协作闭环参与方角色示例用户提出需求“今天北京天气怎么样”模型LLM理解意图决定调用哪个工具生成参数输出{name: get_weather, arguments: {location: 北京}}应用程序执行工具函数将结果返回给模型调用天气API得到“晴22°C”再交给模型生成最终回复这个闭环的核心是模型不直接执行代码它只输出一个“调用请求”tool_calls真正的执行由应用程序完成。为什么这样设计原因很简单模型无法安全地执行任意代码会产生巨大的安全风险也无法直接访问外部系统没有网络权限、数据库连接等。让模型“提议”让应用程序“执行”是最安全、最可控的分工方式。二、Function Calling的完整数据链路2.1 工具的定义JSON Schema规范为了让模型理解“有哪些工具可用”我们需要用JSON Schema描述每个工具的名称、描述和参数结构。以“获取天气”工具为例{ type: function, function: { name: get_weather, description: 获取指定城市的当前天气信息返回温度、天气状况和空气质量, parameters: { type: object, properties: { location: { type: string, description: 城市名称例如北京、上海、广州 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位默认为celsius } }, required: [location] } } }这个JSON会作为请求的一部分发送给模型告诉模型“当用户询问天气时你可以调用这个函数需要提供location参数unit是可选的。”2.2 模型的输出tool_calls当模型判断需要调用工具时它的响应不是直接输出文本而是输出一个结构化的tool_calls对象{ choices: [{ message: { role: assistant, content: null, // 模型没有直接回复文本而是选择调用工具 tool_calls: [{ id: call_abc123, type: function, function: { name: get_weather, arguments: {\location\: \北京\, \unit\: \celsius\} } }] } }] }关键字段tool_calls一个数组表示模型希望调用的工具一个请求可以调用多个工具function.name要调用的函数名function.argumentsJSON字符串包含调用参数id本次调用的唯一标识用于后续关联工具结果2.3 应用程序执行工具应用程序收到tool_calls后根据name路由到对应的函数def execute_tool_call(tool_call): if tool_call.function.name get_weather: args json.loads(tool_call.function.arguments) location args.get(location) unit args.get(unit, celsius) return get_weather(location, unit) # 其他工具... raise ValueError(fUnknown tool: {tool_call.function.name})2.4 工具结果的回传执行完成后应用程序需要将结果以tool角色的消息追加到对话历史中messages.append({ role: tool, tool_call_id: call_abc123, # 必须与原始调用的id一致 content: 北京当前天气晴温度22°C空气质量良好 })然后再次调用模型将包含工具结果的完整历史发过去让模型基于工具返回的数据生成最终的自然语言回复。2.5 完整的多轮闭环一次完整的工具调用涉及两次API请求第一次请求带tools定义 用户消息 → 模型 → 返回tool_calls无文本内容 第二次请求不带tools或仍带但模型不再调用 用户消息 模型之前的tool_calls 工具执行结果 → 模型 → 返回最终文本如果需要多个工具或工具结果不满足需求这个循环可能持续多次模型可能连续调用多个工具。三、工具定义的最佳实践工具定义的优劣直接影响模型的调用准确率。以下是最佳实践3.1 name命名的三原则原则说明好例子坏例子动词优先用动词描述操作get_weatherweather清晰无歧义一个工具只做一件事search_webdo_search_and_summarize拆成两个工具与实现对应名称与函数名一致send_email→def send_email()名称与实现不匹配3.2 description的撰写策略描述是模型判断“何时调用”的核心依据。好的描述应该包含description: 获取指定城市的实时天气信息。 使用场景 - 用户询问当前天气状况 - 用户询问温度、降雨概率、空气质量 - 用户提到“出门是否需要带伞”等与天气相关的问题 不适用场景 - 查询历史天气数据 - 天气预报仅返回当前时刻数据 参数说明 - location: 城市名称需用中文 - unit: 温度单位默认摄氏度 关键原则描述要明确告诉模型什么时候该用、什么时候不该用。这能有效减少模型误用工具的概率。3.3 参数设计的规范parameters: { type: object, properties: { query: { type: string, description: 搜索关键词应使用用户原话中的关键词 }, max_results: { type: integer, description: 返回结果数量默认为5用户明确要求更多时调整, default: 5, minimum: 1, maximum: 20 } }, required: [query] }设计要点提供默认值减少模型必须提供参数的负担限定取值范围用enum、minimum/maximum约束参数避免非法输入明确描述参数语义让模型知道如何从用户问题中提取参数四、自定义Tool的实现CrewAI在CrewAI中自定义工具通过继承BaseTool实现。4.1 博查搜索工具的实现博查搜索是国内可用的免费搜索API适合个人开发者使用。import os import requests from crewai.tools import BaseTool from pydantic import Field from typing import Optional class BochaSearchTool(BaseTool): name: str 博查搜索 description: str ( 搜索互联网信息返回相关网页的标题、摘要和链接。 当用户需要查询实时信息、新闻、最新动态时使用此工具。 ) api_key: Optional[str] Field(defaultNone, description博查API密钥) def __init__(self, **kwargs): super().__init__(**kwargs) self.api_key os.getenv(BOCHA_API_KEY) if not self.api_key: raise ValueError(请在.env中设置BOCHA_API_KEY) def _run(self, query: str) - str: 执行搜索并返回格式化的结果 url https://open.bochaai.com/v1/web-search headers { Content-Type: application/json, Authorization: fBearer {self.api_key} } payload {query: query, count: 5} try: resp requests.post(url, headersheaders, jsonpayload, timeout10) resp.raise_for_status() data resp.json() pages data.get(data, {}).get(webPages, {}).get(value, []) if not pages: return f未找到 {query} 的相关结果。 result f关于 {query} 的搜索结果\n\n for i, p in enumerate(pages, 1): title p.get(name, 无标题) snippet p.get(snippet, 无摘要) link p.get(url, #) result f{i}. {title}\n {snippet}\n 来源: {link}\n\n return result except requests.exceptions.RequestException as e: return f搜索失败: {str(e)}4.2 在Agent中绑定工具from crewai import Agent search_tool BochaSearchTool() researcher Agent( role高级研究员, goal针对指定主题搜索并收集最新资料, backstory你是资深研究员擅长使用搜索引擎快速定位关键信息, tools[search_tool], # 研究员拥有搜索能力 llmllm, verboseTrue )4.3 模拟搜索作为降级方案当API不可用或测试阶段使用模拟搜索可以保证流程不被中断class MockSearchTool(BaseTool): name: str 模拟搜索 description: str 模拟搜索返回示例数据用于测试 def _run(self, query: str) - str: return f关于 {query} 的模拟搜索结果 1. AI Agent发展趋势 2025年多智能体系统在金融、医疗领域的应用快速增长... 来源: https://example.com/ai-agent-trends 2. 大模型工具调用技术解析 Function Calling正在成为Agent与外部世界交互的标准接口... 来源: https://example.com/tool-calling 此为模拟数据请配置BOCHA_API_KEY获取真实结果 def create_search_tool(): 根据环境变量决定使用真实搜索还是模拟 if os.getenv(BOCHA_API_KEY): return BochaSearchTool() else: print(⚠️ 未配置BOCHA_API_KEY使用模拟搜索) return MockSearchTool()五、工具调用的错误处理5.1 工具执行失败的场景失败场景原因处理方式API密钥无效未配置或配置错误返回友好错误信息引导用户检查配置网络超时API服务不可达重试或返回超时提示参数解析错误模型生成的参数格式不正确捕获JSON解析异常返回错误信息工具不存在模型调用了未注册的工具记录日志返回“工具不可用”5.2 错误处理代码模板async def execute_tool_call(tool_call) - str: try: if tool_call.function.name get_weather: args json.loads(tool_call.function.arguments) return get_weather(**args) else: return f错误未知工具 {tool_call.function.name} except json.JSONDecodeError: return f错误参数解析失败请检查工具定义 except KeyError as e: return f错误缺少必需参数 {e} except Exception as e: return f错误工具执行失败 - {str(e)}六、Agentic Loop的完整实现def agentic_loop(user_message: str, tools: list, max_iterations: int 5): 完整的Agentic Loop实现 messages [{role: user, content: user_message}] for _ in range(max_iterations): # 第一次调用可能返回tool_calls response client.chat.completions.create( modeldeepseek-v4-flash, messagesmessages, toolstools, tool_choiceauto ) message response.choices[0].message # 没有工具调用 → 直接返回 if not message.tool_calls: return message.content # 有工具调用 → 执行并追加结果 messages.append(message.model_dump()) for tool_call in message.tool_calls: result execute_tool_call(tool_call) messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) # 继续循环再次调用模型 return 达到最大迭代次数未完成七、深度思考工具调用的关键理解7.1 模型不会“执行”工具只会“请求”工具这是理解Function Calling最重要的概念。整个链条中模型只做了两件事判断“当前是否需要调用工具”生成结构化的tool_callsJSON模型没有执行任何代码没有访问任何外部系统。所有的执行都发生在应用程序侧。这意味着安全可控你可以对工具调用进行审计、限流、权限校验灵活扩展可以添加任何类型的工具API调用、数据库查询、本地命令只要应用程序能执行7.2 tool_choice的三个选项选项行为适用场景auto模型自主决定是否调用工具通用场景推荐none强制模型不调用工具简单对话不需要工具{type: function, function: {name: xxx}}强制调用指定工具确定性场景如“必须查天气”7.3 多个tool_calls的处理模型一次可以返回多个tool_calls多个工具并行调用。应用程序需要并发执行所有工具asyncio.gather按顺序将结果追加到messages再次调用模型获取最终回复思考与动手建议用curl命令调用DeepSeek API手动构造一个包含工具定义和用户消息的请求观察返回的tool_calls结构。为你的Agent添加两个工具一个搜索工具和一个计算器工具观察模型如何根据用户问题选择合适的工具。尝试构造一个“工具调用失败”的场景观察Agent是否能够理解错误信息并给出合理的反馈。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进