第七章:Function Call 与工具调用

Function Call是AI应用开发最核心的基础能力。本节课详解LLM工具调用原理、完整流程及在三大框架中的实现方式。

这是 AI 应用开发中最核心的基础能力 — 让 LLM 能调用外部工具。

7.1 什么是 Function Call?

Function Call(函数调用 / 工具调用)是 LLM 的一种能力:模型不再只是”生成文字”,而是可以决定调用一个函数,并生成调用所需的参数。

类比 Web 开发:就像前端调后端 API — 用户点击按钮,前端决定调用哪个 API、传什么参数。在 AI 应用中,LLM 就是”前端”,工具函数就是”后端 API”。

1
2
3
4
5
传统 LLM:
用户 → LLM → 文字回答

Function Call:
用户 → LLM → "我需要调用 search('北京天气')" → 执行工具 → 结果返回 LLM → 文字回答

Function Call 完整流程:

sequenceDiagram
participant User as 用户
participant App as 应用层
participant LLM as LLM API
participant Tool as 工具函数

User->>App: 发送消息
App->>LLM: 发送消息 + 工具定义
LLM->>LLM: 分析是否需要调用工具
alt 需要调用工具
    LLM-->>App: 返回 tool_call(name, arguments)
    App->>Tool: 执行工具函数
    Tool-->>App: 返回工具结果
    App->>LLM: 追加工具结果到消息历史
    LLM-->>App: 基于结果生成最终回答
else 不需要工具
    LLM-->>App: 直接生成回答
end
App-->>User: 返回回答

7.2 工具调用的工作原理

flowchart TD
A[1. 用户发送消息] --> B[2. LLM 分析消息]
B --> C{3. 需要调用工具?}
C -->|是| D["4. 生成 tool_call<br/>函数名 + 参数"]
D --> E[5. 应用层执行工具]
E --> F[6. 将结果追加到消息历史]
F --> G[7. LLM 基于结果生成回答]
C -->|否| G
G --> H[返回最终回答]

style A fill:#e1f5fe
style D fill:#fff9c4
style E fill:#f3e5f5
style G fill:#e8f5e9

关键理解:LLM 不执行工具,它只是决定调用哪个工具和传什么参数。执行是应用层的事。

7.3 三种定义工具的方式

方式一:普通 Python 函数(DeepAgents 推荐)

最简单的方式 — 写一个普通函数,加个文档字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_weather(city: str) -> str:
"""获取指定城市的天气信息

Args:
city: 城市名称,如 '北京'、'上海'

Returns:
天气信息字符串
"""
return f"{city}今天晴,25°C"

# DeepAgents 直接使用
agent = create_deep_agent(
model="openai:gpt-4o",
tools=[get_weather],
)

要点

  • 函数名就是工具名
  • 文档字符串(docstring)告诉 LLM 这个工具做什么、什么时候用
  • 参数类型注解(city: str)告诉 LLM 参数的类型
  • 文档字符串非常重要 — 它决定了 LLM 是否能正确选择工具

方式二:@tool 装饰器(LangChain 通用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from langchain_core.tools import tool

@tool
def search_database(query: str, limit: int = 10) -> str:
"""搜索数据库中的记录

Args:
query: 搜索关键词
limit: 返回结果数量,默认10
"""
# 实际的搜索逻辑
results = db.search(query, limit=limit)
return str(results)

# LangChain 使用
llm = ChatOpenAI(model="gpt-4o")
llm_with_tools = llm.bind_tools([search_database])

@tool 的额外能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain_core.tools import tool
from pydantic import BaseModel, Field

class SearchInput(BaseModel):
"""搜索输入参数"""
query: str = Field(description="搜索关键词")
limit: int = Field(default=10, description="返回结果数量,最大100")
category: str = Field(default="all", description="搜索类别:all/news/tech")

@tool(args_schema=SearchInput)
def search_database(query: str, limit: int = 10, category: str = "all") -> str:
"""搜索数据库中的记录"""
return f"搜索 '{query}',类别 {category},限制 {limit} 条"

# 返回值也可以自定义解析
@tool(response_format="content_and_artifact")
def search_with_metadata(query: str) -> tuple[str, dict]:
"""搜索并返回元数据"""
content = "搜索结果..."
artifact = {"total": 100, "page": 1}
return content, artifact

方式三:StructuredTool 类(最灵活)

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
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field

class SQLQueryInput(BaseModel):
"""SQL 查询输入"""
query: str = Field(description="要执行的 SQL SELECT 语句")
database: str = Field(default="main", description="数据库名称")

def execute_sql(query: str, database: str = "main") -> str:
return db.execute(query)

def handle_error(error: Exception) -> str:
return f"查询错误: {str(error)}"

sql_tool = StructuredTool.from_function(
func=execute_sql,
name="sql_query",
description="执行 SQL 查询并返回结果。只允许 SELECT 语句。",
args_schema=SQLQueryInput,
handle_error=handle_error,
return_direct=False, # True = 工具结果直接返回给用户,不再经过 LLM
)

# 使用
agent = create_deep_agent(model="openai:gpt-4o", tools=[sql_tool])

7.4 工具调用的底层协议

当你调用 llm.bind_tools([get_weather]) 时,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
# 1. 从函数签名提取工具 Schema
tool_schema = {
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气信息",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 '北京'、'上海'"
}
},
"required": ["city"]
}
}
}

# 2. 发送给 LLM 的请求
response = openai.chat.completions.create(
model="gpt-4o",
messages=[...],
tools=[tool_schema], # 工具定义
)

# 3. LLM 返回的 tool_call
response.choices[0].message.tool_calls[0]
# → {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}

# 4. 应用层执行工具
result = get_weather(city="北京")

# 5. 将结果追加到消息历史
messages.append({"role": "tool", "content": result, "tool_call_id": "..."})

# 6. 再次调用 LLM,让它基于工具结果生成回答

7.5 编写高质量工具的最佳实践

1. 文档字符串决定一切

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 差 — LLM 不知道什么时候该用这个工具
def search(q: str) -> str:
return "结果"

# 好 — LLM 知道什么时候用、怎么用
def search_product_by_name(
product_name: str,
category: str = "all",
max_price: float = None,
) -> str:
"""根据商品名称搜索商品。当用户询问商品信息、价格、库存时使用此工具。

Args:
product_name: 商品名称或关键词
category: 商品类别,可选值:all/electronics/clothing/food
max_price: 最高价格筛选,为空表示不限

Returns:
匹配的商品列表
"""

2. 参数设计要清晰

1
2
3
4
5
6
7
8
9
# 差 — 容易混淆
def query(type: str, value: str) -> str: ...

# 好 — 参数含义清晰
def query_customer_by_phone(phone: str) -> str:
"""根据手机号查询客户信息"""

def query_order_by_id(order_id: str) -> str:
"""根据订单号查询订单"""

3. 错误处理要友好

1
2
3
4
5
6
7
8
9
10
def run_sql(query: str) -> str:
"""执行 SQL 查询"""
try:
return db.execute(query)
except SyntaxError as e:
return f"SQL 语法错误: {e}。请检查 SQL 语句后重试。"
except PermissionError:
return "权限不足:只允许执行 SELECT 查询。"
except Exception as e:
return f"查询执行失败: {e}"

4. 返回值要简洁

1
2
3
4
5
6
7
8
9
10
# 差 — 返回大量原始数据
def search(query: str) -> dict:
return database.raw_query(query) # 可能返回几 MB 数据

# 好 — 返回摘要 + 分页
def search(query: str, page: int = 1, page_size: int = 10) -> str:
"""搜索数据,返回分页结果"""
results = database.query(query, offset=(page-1)*page_size, limit=page_size)
total = database.count(query)
return f"共 {total} 条结果,第 {page} 页:\n" + format_results(results)

7.6 运行时上下文注入

在 DeepAgents 中,工具可以访问运行时上下文(如用户 ID、API Key):

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
from dataclasses import dataclass
from deepagents import create_deep_agent
from langchain.tools import tool

@dataclass
class AppContext:
user_id: str
api_key: str

@tool
def fetch_user_orders(query: str, runtime) -> str:
"""查询当前用户的订单"""
user_id = runtime.context.user_id # 从运行时上下文获取用户 ID
return orders_db.query(user_id=user_id, keyword=query)

agent = create_deep_agent(
model="openai:gpt-4o",
tools=[fetch_user_orders],
context_schema=AppContext,
)

# 调用时传入上下文
result = agent.invoke(
{"messages": [{"role": "user", "content": "我的订单有哪些?"}]},
config={"context": AppContext(user_id="u123", api_key="key-xxx")},
)

7.7 多工具协调

当 Agent 有多个工具时,LLM 会自主决定调用哪个(或哪几个):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def search_products(query: str) -> str:
"""搜索商品"""
return f"找到3个商品: {query}"

def check_inventory(product_id: str) -> str:
"""查询商品库存"""
return f"商品 {product_id} 库存: 50"

def create_order(product_id: str, quantity: int) -> str:
"""创建订单"""
return f"订单已创建: {product_id} x {quantity}"

agent = create_deep_agent(
model="openai:gpt-4o",
tools=[search_products, check_inventory, create_order],
interrupt_on={
"create_order": True, # 创建订单前需要人类审批
},
system_prompt="你是一个购物助手。帮用户搜索商品、查库存、下单。",
)

LLM 可能会按顺序调用:search_productscheck_inventory → 请求人类审批 → create_order

7.8 Function Call 的局限性

局限说明
每个厂商格式不同OpenAI、Anthropic、Google 的 tool_call 格式各异
没有工具发现机制工具必须预先注册,不能动态发现
没有标准协议无法跨应用/跨 Agent 共享工具
缺乏上下文管理工具不知道调用者是谁、之前做了什么
不支持流式调用工具执行是同步阻塞的

这些局限性正是 MCP(模型上下文协议)要解决的问题 → 详见 08-MCP协议入门.md