Research note / 2026/07/07
工具使用 + RAG(检索增强生成)
Agent应用开发 · Day 4 学习笔记

日期:2026-06-06
主题:工具使用(Tool Use)+ RAG(检索增强生成)
进度:24/40 概念
一、Function Calling 机制
核心概念
Function Calling = LLM 决定调用哪个工具、传什么参数,但不执行工具。执行由你的代码完成。
用户:"帮我查北京天气"
→ LLM 输出:我应该调用 get_weather(city="北京")
→ 你的代码:执行 get_weather("北京") → 返回结果
→ LLM 用结果生成最终回复
完整流程
1. 用户发消息
2. 你把用户消息 + 工具定义 一起发给 LLM
3. LLM 返回:{ "tool_calls": [{ "name": "get_weather", "arguments": {"city": "北京"} }] }
4. 你的代码执行 get_weather("北京") → {"temp": "22°C", "weather": "晴"}
5. 你把工具结果发回 LLM
6. LLM 生成最终回复:"北京今天22°C,天气晴朗。"
关键理解
要点说明LLM 不执行工具它只输出”我想调用什么”,执行权在你工具定义是提示词的一部分工具描述写得好不好,直接影响 LLM 选对工具的概率多轮对话一次请求可能触发多次工具调用并行调用有些模型支持一次返回多个 tool_calls
API 调用示例(OpenAI)
import openai
# 1. 定义工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名称"},
"date": {"type": "string", "description": "日期,如 '明天'"}
},
"required": ["city"]
}
}
}
]
# 2. 发送请求
response = openai.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "北京明天天气怎么样?"}],
tools=tools,
tool_choice="auto" # 让模型自己决定要不要调工具
)
# 3. 检查是否有工具调用
message = response.choices[0].message
if message.tool_calls:
for call in message.tool_calls:
func_name = call.function.name # "get_weather"
args = json.loads(call.function.arguments) # {"city": "北京", "date": "明天"}
result = execute_function(func_name, args)
# 4. 把结果发回 LLM
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
tool_choice 参数
值行为适用场景"auto"模型自己决定默认,最灵活"none"强制不调工具最终回复阶段"required"强制必须调工具确保模型不直接回答{"type": "function", "function": {"name": "xxx"}}强制调指定工具测试、特殊流程
二、工具定义与描述规范
工具描述的黄金法则
工具描述 = 写给 LLM 的”产品说明书”。描述不清,LLM 就会选错工具。
好的工具定义三要素
1. 名称(name):动词_名词,一看就知道干什么
2. 描述(description):什么时候该用,什么时候不该用
3. 参数(parameters):每个参数的类型、含义、是否必填
差 vs 好的对比
❌ 差:
{
"name": "search",
"description": "搜索",
"parameters": {
"type": "object",
"properties": {
"q": {"type": "string"}
}
}
}
→ LLM 不知道搜什么、什么时候用、参数什么意思
✅ 好:
{
"name": "search_products",
"description": "在商品库中搜索商品。当用户询问商品信息、价格、库存时使用。不要用于查询订单状态。",
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "搜索关键词,如商品名称或类别"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food", "other"],
"description": "商品类别筛选"
},
"max_price": {
"type": "number",
"description": "价格上限(元)"
}
},
"required": ["keyword"]
}
}
描述中的关键技巧
① 写”什么时候用”
"description": "查询订单物流状态。当用户问'我的快递到哪了'、'什么时候到'时使用。"
② 写”什么时候不用”
"description": "查询订单物流状态。不要用于取消订单或修改地址。"
③ 用 enum 约束可选值
"status": {
"type": "string",
"enum": ["pending", "shipped", "delivered", "cancelled"],
"description": "订单状态"
}
→ 防止 LLM 输出不存在的值
④ 参数互相排斥时要说明
"description": "搜索商品。keyword 和 category 至少提供一个。"
工具数量的权衡
工具数量影响1-5 个选择准确率高6-15 个开始需要仔细写描述15+ 个准确率下降,考虑动态加载
经验法则:单次请求不超过 10 个工具。超过时根据用户意图动态筛选。
三、工具编排与链式调用
串行调用
前一个工具的输出是后一个工具的输入:
用户:"帮我查最近的航班并预订"
Step 1: search_flights(from="北京", to="上海")
→ 返回航班列表
Step 2: LLM 选择航班 → book_flight(flight_id="CA1234")
→ 返回预订结果
并行调用
多个工具之间没有依赖,可以同时调用:
用户:"对比北京和上海的天气"
同时调用:
- get_weather(city="北京")
- get_weather(city="上海")
→ 两个结果一起返回给 LLM
API 层面的并行:
# LLM 一次返回多个 tool_calls
response.tool_calls = [
{"function": {"name": "get_weather", "arguments": '{"city": "北京"}'}},
{"function": {"name": "get_weather", "arguments": '{"city": "上海"}'}}
]
# 你的代码可以并行执行
import asyncio
results = await asyncio.gather(
execute_tool(call) for call in response.tool_calls
)
条件调用
根据前一个工具的结果决定下一步:
Step 1: check_inventory(product_id="P001")
→ 如果库存 > 0 → Step 2: create_order(...)
→ 如果库存 == 0 → Step 2: notify_restock(...) + suggest_alternative(...)
混合编排模式
实际 Agent 中最常见的模式:
用户:"帮我分析三个竞品的价格策略"
├─ 并行:search_competitor("A") + search_competitor("B") + search_competitor("C")
├─ 等待三个结果全部返回
├─ LLM 分析对比
├─ 条件:如果需要更详细数据
│ └─ 串行:deep_analysis(competitor="A", focus="pricing")
└─ 最终输出对比报告
四、错误处理与重试策略
工具调用会失败的场景
失败类型例子处理策略参数错误LLM 输出了错误的参数格式代码端校验 + 反馈给 LLM 重试工具不存在LLM 幻觉了一个不存在的工具工具列表校验 + 返回错误提示超时API 调用超时设置超时 + 重试 + 降级权限不足工具需要认证但没提供检查配置 + 提示用户业务错误查询的订单不存在把错误信息返回 LLM,让它解释
重试策略
① 简单重试
def call_tool_with_retry(tool_call, max_retries=3):
for attempt in range(max_retries):
try:
result = execute_tool(tool_call)
return result
except ToolError as e:
if attempt == max_retries - 1:
return {"error": str(e)}
time.sleep(2 ** attempt) # 指数退避
② 让 LLM 修正参数重试
def call_tool_with_llm_retry(tool_call, messages):
result = execute_tool(tool_call)
if "error" in result:
# 把错误告诉 LLM,让它修正参数
error_msg = f"工具调用失败:{result['error']}。请检查参数并重试。"
messages.append({"role": "tool", "content": error_msg})
new_response = call_llm(messages)
# LLM 可能输出修正后的 tool_call
return handle_tool_calls(new_response, messages)
return result
③ 降级策略
def search_with_fallback(query):
# 优先用精确搜索
results = precise_search(query)
if not results:
# 降级到模糊搜索
results = fuzzy_search(query)
if not results:
# 最终降级到 LLM 直接回答
return {"fallback": "llm_direct"}
return results
把错误信息返回 LLM 的重要性
工具失败时,一定要把错误原因告诉 LLM,而不是静默吞掉。
❌ 静默吞掉:tool_result = "查询失败" → LLM 不知道为什么失败,可能编造数据
✅ 告诉原因:tool_result = "查询失败:API 返回 429,请求过于频繁" → LLM 可以建议用户稍后重试
五、文档分块策略(RAG 基础)
什么是 RAG
RAG = Retrieval-Augmented Generation(检索增强生成)
用户提问 → 检索相关文档 → 把文档 + 问题一起给 LLM → LLM 基于文档回答
解决的问题:LLM 训练数据有截止日期、不了解你的私有数据。
为什么要分块
LLM 上下文窗口有限,不能把整本书塞进去。需要把文档切成小块,只检索相关的几块。
三种分块策略
① 固定长度分块
def chunk_by_size(text, chunk_size=500, overlap=50):
chunks = []
for i in range(0, len(text), chunk_size - overlap):
chunks.append(text[i:i + chunk_size])
return chunks
优点缺点实现简单可能在句子中间切断块大小可控语义不完整
② 递归分块(推荐)
def recursive_chunk(text, chunk_size=500):
# 优先按大分隔符切,不行再按小分隔符
separators = ["\n\n", "\n", "。", ".", " "]
# 递归尝试每个分隔符
...
按语义边界切分:先尝试段落 → 再尝试句子 → 最后按字符。
优点缺点保留语义完整性实现稍复杂LangChain 默认策略块大小不完全均匀
③ 语义分块
根据语义相似度切分——当相邻句子的语义差异变大时,切一刀。
# 伪代码
for i in range(len(sentences)):
similarity = embed(sentences[i]) · embed(sentences[i+1])
if similarity < threshold:
chunk_here() # 语义变了,切分
优点缺点语义最完整需要计算嵌入,慢检索效果最好实现复杂
分块参数选择
参数推荐值说明chunk_size200-1000 tokens太小丢上下文,太大不精确chunk_overlap10-20% 的 chunk_size重叠防止边界丢失信息
元数据标注
每个块附带来源信息,方便引用:
chunk = {
"text": "Agent 是能自主决策的系统...",
"metadata": {
"source": "agent_intro.pdf",
"page": 3,
"chapter": "第一章"
}
}
六、向量嵌入与相似度检索
Embedding 是什么
把文本转换成一个数字向量(一串浮点数),语义相似的文本在向量空间中距离近。
"猫" → [0.2, 0.8, 0.1, ...]
"小猫" → [0.21, 0.79, 0.12, ...] ← 距离近
"汽车" → [0.9, 0.1, 0.7, ...] ← 距离远
相似度计算
余弦相似度(最常用):
import numpy as np
def cosine_similarity(a, b):
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
# 计算两个文本的相似度
sim = cosine_similarity(embed("猫"), embed("小猫")) # 0.95(很相似)
sim = cosine_similarity(embed("猫"), embed("汽车")) # 0.15(不相似)
RAG 检索流程
1. 文档分块 → [chunk1, chunk2, ...]
2. 每个块做 Embedding → [vec1, vec2, ...]
3. 存入向量数据库
4. 用户提问 → 问题也做 Embedding → query_vec
5. 在向量数据库中找最相似的 top-k 个块
6. 把这些块 + 用户问题一起给 LLM
向量数据库选型
数据库特点适用场景ChromaDB轻量、嵌入式、Python 原型快速原型、小数据量Qdrant高性能、Rust 实现生产环境、中大规模Pinecone全托管、无需运维不想自己部署Milvus开源、高度可扩展大规模生产pgvectorPostgreSQL 扩展已有 PG 基础设施
Embedding 模型选型
模型维度特点OpenAI text-embedding-3-small1536性价比高OpenAI text-embedding-3-large3072效果最好BGE-M31024开源、多语言nomic-embed-text768开源、轻量
ChromaDB 快速上手
import chromadb
# 1. 创建客户端和集合
client = chromadb.Client()
collection = client.create_collection("my_docs")
# 2. 添加文档(ChromaDB 自动做 Embedding)
collection.add(
documents=["Agent 是能自主决策的系统", "RAG 是检索增强生成技术"],
ids=["doc1", "doc2"]
)
# 3. 查询
results = collection.query(
query_texts=["什么是 Agent?"],
n_results=2
)
# 返回最相关的 2 个文档块
七、Day 4 核心技能总结
- Function Calling = LLM 决策 + 代码执行:LLM 只输出调用意图,不执行工具
- 工具描述是关键:写清楚什么时候用、什么时候不用、参数含义
- 工具数量要控制:单次不超过 10 个,超过时动态加载
- 串行/并行/条件三种编排模式:根据依赖关系选择
- 错误必须反馈给 LLM:让它知道为什么失败,才能修正或降级
- 递归分块是默认选择:按语义边界切分,保留上下文完整性
- 向量检索 = Embedding + 余弦相似度:语义相似的文本距离近
- ChromaDB 适合快速原型:生产环境考虑 Qdrant 或 Milvus
Discussion
评论
站点尚未配置 GitHub Discussions。文章阅读不受影响,也不会再发起无效的 GitHub 登录请求。