当 LLM 不再只是"说话模型",而是能够调用搜索、浏览、对比、润色等实际工具的自主 Agent,智能客服系统的能力边界就从"回答问题"扩展到了"解决问题"。本文深入解析一个某个 FAQ 系统中的 Tool-Augmented Agent 设计------从 10 个工具的精细定义到 ReAct 循环的并发安全实现,展示如何构建生产级的工具调用引擎。
一、为什么需要 Tool-Augmented Agent?
传统 RAG(检索增强生成)系统的回答流程是固定的:收到问题 → 检索知识库 → 拼接上下文 → 生成答案。这种流水线在面对某个产品操作问答时,存在几个硬伤:
- 检索目标不明确:用户问"系统X和系统B在审批流程上有什么不同?",系统不知道该用哪个文档范围,或者两个都搜然后对比。
- 无法自主探索:用户说"告诉我系统X都有哪些模块",系统需要先浏览索引结构,再决定读哪个章节,而非一次性盲目检索。
- 一步到位的幻觉风险:如果一次检索的结果不够好(得分低),系统没有"换种方式再搜"的自救机制。
- 无法输出专业化结果:原始文档的语言偏技术化,直接返回给终端用户显得生硬,需要专业润色。
Tool-Augmented Agent(工具增强型 Agent) 的解决思路是:把每一项原子能力封装为一个独立的工具,让 LLM 充当"调度员",根据问题类型自主选择工具组合,在 ReAct(Reasoning + Acting)循环中逐步逼近最优答案。
用户问题
│
▼
┌─────────────────────────────────────────┐
│ LLM 推理(Think) │
│ ├─ 判断当前状态 │
│ ├─ 选择下一步工具 │
│ └─ 生成 tool_call 参数 │
├─────────────────────────────────────────┤
│ 工具执行(Act) │
│ ├─ 匹配 _TOOL_EXECUTORS │
│ ├─ 执行获取结果 │
│ └─ 返回观察(Observation) │
├─────────────────────────────────────────┤
│ 重复以上循环 → 直到 LLM 自然回答 │
└─────────────────────────────────────────┘
│
▼
最终回答
二、10 个工具的设计体系
系统定义了 10 个工具,全部采用 OpenAI Function Calling 格式 ,且设置 strict=true 启用结构化输出。这 10 个工具按职责分为四大类:
2.1 A 类:知识检索(核心能力)
这是系统的"拳头工具组",覆盖了从搜索到定位到原文读取的完整检索链路。
① search_knowledge ------ 知识库语义搜索
json
{
"name": "search_knowledge",
"strict": true,
"description": "搜索产品操作知识库,返回最相关的文档片段。适用于操作步骤查询、功能说明查询等明确的业务问题",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索查询语句,应提取问题的核心关键词"
},
"system": {
"type": "string",
"enum": ["系统X", "系统B", "系统C", "auto"],
"description": "指定查询哪个业务系统,auto 表示自动识别"
}
},
"required": ["query", "system"]
}
}
这是最核心的检索工具,直接调用系统的多层检索引擎(navigate/index/direct 三种模式,6 种匹配算法)。LLM 除了提供搜索关键词外,还需要指定目标业务系统,这利用了系统的 target_systems 归属判断能力,在工具层面缩小检索范围,提升命中率。
system 参数的 auto 选项允许 LLM 在不确定归属时让系统自动识别,相当于给模型留了一个"不知道就交给系统判断"的兜底通道。
② browse_index ------ 浏览文档索引结构
json
{
"name": "browse_index",
"strict": true,
"description": "浏览知识库的文档索引结构,回答'有哪些模块''包含什么功能'等结构类问题",
"parameters": {
"type": "object",
"properties": {
"system": {
"type": "string",
"enum": ["系统X", "系统B", "系统C", "all"],
"description": "指定查询哪个业务系统的索引"
}
},
"required": ["system"]
}
}
设计初衷 :当用户问"系统X有哪些功能模块?"这类概括性问题时,直接执行 search_knowledge 可能会返回碎片化的结果。browse_index 直接读取文档的索引导航结构,返回类似目录树的信息,让用户(和 LLM 自己)先建立对文档结构的全局认知。
这个工具与系统的索引文件格式紧密配合------每个索引文件都包含了章-节-条的层级结构、摘要、关键词和可能的提问示例。
③ read_section ------ 读取指定章节原文
json
{
"name": "read_section",
"strict": true,
"description": "读取知识库中指定章节的原文内容。当 search_knowledge 返回的片段不够详细时,可以精确读取该章节的完整原文",
"parameters": {
"type": "object",
"properties": {
"system": {
"type": "string",
"enum": ["系统X", "系统B", "系统C"],
"description": "所属业务系统"
},
"chapter": {
"type": "string",
"description": "章编号,如'第1章'"
},
"section": {
"type": "string",
"description": "节编号,如'第3节'"
},
"article": {
"type": "string",
"description": "条编号,如'第5条'"
}
},
"required": ["system", "chapter", "section"]
}
}
定位策略 :search_knowledge 返回的是片段(可能是截断的或经过粗筛的),当 LLM 觉得需要更完整的上下文时,read_section 提供"拉取原始全文"的能力。参数设计采用 章/节/条 三级定位,与知识库文档的层级结构一一对应,确保定位精确到段落级别。
④ search_memory ------ 搜索历史高分回答
json
{
"name": "search_memory",
"strict": true,
"description": "在历史高分问答记录中搜索,适用于高频重复问题。评分越高、匹配度越高的记录优先返回",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索查询语句"
}
},
"required": ["query"]
}
}
这是 L2 记忆层的工具化封装。当用户问的问题在历史中已经被解答过且获得了高分(例如"如何创建工单"这样的高频问题),直接从记忆库取答案比重新检索知识库快得多------既节省 Token,又利用了过去已验证的优质答案。
搜索排序逻辑:匹配分数 × TTL 衰减 × 客户评分。系统支持三种 TTL 策略(hard/linear/exponential),让记忆既有"保鲜期"又不会突然消失。
⑤ web_search ------ 互联网搜索兜底
json
{
"name": "web_search",
"strict": true,
"description": "当知识库中找不到答案时,使用互联网搜索兜底,适用于时效性强的政策法规、市场行情等问题",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索查询语句"
},
"count": {
"type": "number",
"description": "返回结果数量,默认为 3"
}
},
"required": ["query"]
}
}
兜底设计原则 :web_search 位于工具列表的最后优先级。LLM 的 TOOL_USE_SYSTEM_PROMPT 明确指导------只有在知识库搜索返回空结果或质量评分极低时,才启用互联网搜索。结果返回后需标注来源 URL,确保可追溯。
2.2 B 类:问题处理(推理增强)
⑥ decompose_question ------ 复杂问题拆解
json
{
"name": "decompose_question",
"strict": true,
"description": "当用户问题包含多个独立子问题时,拆解为多个子问题分别检索后再合成答案",
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "需要拆解的原始问题"
},
"sub_questions": {
"type": "array",
"items": {"type": "string"},
"description": "拆解后的子问题列表"
}
},
"required": ["question", "sub_questions"]
}
}
设计要点 :这个工具的输出不是直接返回给用户,而是作为 ReAct 循环中的中间步骤。LLM 调用此工具获得子问题列表后,继续对每个子问题调用 search_knowledge 或 browse_index 获取答案,最后汇总。这实际上实现了策略路由中策略 B 的"先规划再执行"思想,但是在工具层面而非策略层面。
⑦ compare_sections ------ 章节内容对比
json
{
"name": "compare_sections",
"strict": true,
"description": "读取两个不同章节的原文内容并对比,适用于'系统X和系统B的区别'等对比类问题",
"parameters": {
"type": "object",
"properties": {
"sections": {
"type": "array",
"items": {
"type": "object",
"properties": {
"system": {"type": "string"},
"chapter": {"type": "string"},
"section": {"type": "string"},
"article": {"type": "string"}
},
"required": ["system", "chapter", "section"]
},
"description": "需要对比的章节列表,支持2-3个章节"
},
"compare_dimension": {
"type": "string",
"description": "对比维度,如'功能差异''流程差异'"
}
},
"required": ["sections", "compare_dimension"]
}
}
这是一个复合工具 ------内部先调用 read_section 读取每个章节的原文,再用 LLM 进行对比分析。之所以设计为独立工具而非让 LLM 自己组合 read_section × N,是因为对比分析需要专门的"求同存异"视角,与普通阅读不同。封装为工具后,LLM 只需提供章节定位和对比维度,系统负责读取和对比的编排。
2.3 C 类:输出处理
⑧ polish_answer ------ 客服风格润色
json
{
"name": "polish_answer",
"strict": true,
"description": "将技术化的原始答案润色为专业、友好的客服风格,添加适当的引导语和结束语",
"parameters": {
"type": "object",
"properties": {
"answer": {
"type": "string",
"description": "需要润色的原始答案文本"
},
"source": {
"type": "string",
"description": "答案来源,如'系统X产品操作知识库-第2章第3节'"
}
},
"required": ["answer"]
}
}
角色转换 :知识库文档的语言偏技术操作手册风格------"点击XX按钮→选择XX选项→确认保存"。而客服风格需要更友好------"请您点击页面右上角的 XX 按钮,然后选择 XX 选项,最后点击确认即可完成保存"。polish_answer 工具让 LLM 在最终输出前进行一次风格转换,同时自动标注来源章节,增加可信度。
2.4 D 类:系统工具
⑨ list_documents ------ 文档全貌概览
json
{
"name": "list_documents",
"strict": true,
"description": "列出知识库中所有文档及其章节结构,适用于'你们有哪些文档''整个系统包含什么'等全景问题",
"parameters": {
"type": "object",
"properties": {}
}
}
与 browse_index 按系统浏览不同,list_documents 返回所有业务系统的完整文档清单和章节结构。无参数设计意味着它是一次"全量快照",适合 LLM 在不确定用户问题归属时先建立全局视图。
⑩ transfer_to_human ------ 转人工客服
json
{
"name": "transfer_to_human",
"strict": true,
"description": "当无法回答或用户明确要求转人工时,记录转人工事件并移交",
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "转人工的原因说明"
},
"question": {
"type": "string",
"description": "原始用户问题"
}
},
"required": ["reason", "question"]
}
}
最后一道防线:当 LLM 经过多轮 ReAct 循环后仍无法给出满意答案,或者用户的情绪明显需要人工介入时,调用此工具。系统会将转人工事件记录到 CSV 文件(包含时间戳、用户问题、转人工原因),方便客服团队后续跟进和分析。
10 个工具全景总览
| 分类 | 工具 | 输入 | 输出 | 适用场景 |
|---|---|---|---|---|
| A. 知识检索 | search_knowledge | query, system | 文档片段 | 操作步骤/功能说明 |
| browse_index | system | 索引结构 | "有哪些模块"结构类 | |
| read_section | system, chapter, section | 完整原文 | 需要更详细上下文 | |
| search_memory | query | 历史高分问答 | 高频重复问题 | |
| web_search | query, count | 搜索结果 | 知识库未命中兜底 | |
| B. 问题处理 | decompose_question | question | 子问题列表 | 复杂多意图问题 |
| compare_sections | sections\[\], dimension | 对比分析 | "A与B的区别" | |
| C. 输出处理 | polish_answer | answer, source | 润色后答案 | 技术→客服风格 |
| D. 系统工具 | list_documents | 无 | 全文档清单 | 全景概览 |
| transfer_to_human | reason, question | 转人工记录 | 无法回答/用户要求 |
三、ReAct 循环的实现
Tool-Augmented Agent 的核心执行引擎是一个 ReAct 循环 ,实现在 tool_augmented_ask() 函数中。
3.1 主循环流程
python
# 伪代码: ReAct 循环主逻辑
def tool_augmented_ask(question, context):
"""Tool-Augmented Agent 主入口"""
# 构建初始消息序列(含 system prompt 和历史)
messages = build_initial_messages(question, context)
for iteration in range(MAX_ITERATIONS):
# Step 1: 调用 LLM(传入工具定义,让 LLM 自主选择)
response = call_llm_with_tools(messages, TOOL_DEFINITIONS)
# Step 2: 检查 LLM 是否选择调用工具
if not response.has_tool_calls():
# LLM 认为信息足够,直接给出最终回答
return response.content
# Step 3: 执行 LLM 选择的工具
for tool_call in response.tool_calls:
# 解析工具名称和参数
tool_name = tool_call.get("name")
tool_args = parse_arguments(tool_call.get("arguments"))
# 从注册表中查找并执行工具
executor = find_executor(tool_name)
result = executor(**tool_args) if executor else "未知工具"
# 将工具执行结果追加回消息序列(observation)
messages.append(create_tool_message(tool_call.id, result))
# 进入下一轮循环,LLM 看到 tool 结果后决定下一步
# 超过最大迭代次数,转人工
return "抱歉,我暂时无法回答这个问题,已为您转接人工客服。"
**关键设计决策**:
1. **循环终止条件**:唯一终止信号是 LLM 选择**不生成 tool_calls**,直接以自然语言回答。这意味着 LLM 自主判断"我已经收集到足够信息了"。
2. **最大迭代保护**:`max_iterations=10` 防止无限循环,达到上限自动转人工。
3. **消息序列增长**:每次工具调用的结果以 `role="tool"` 追加到消息列表,LLM 的每一轮推理都能看到之前的工具结果(类似思维链的"观察"环节)。
### 3.2 流式工具调用处理
对于支持流式 thinking 模式的 LLM,系统实现了 `_collect_tool_stream()` 方法:
```python
# 伪代码: 流式 tool_calls 的增量收集
# 背景:流式 API 将 tool_calls 分多个 chunk 发送
# 每个 chunk 只携带增量数据,需要拼接完整
def collect_tool_calls_from_stream(response_stream):
"""
对流式响应中的 tool_calls 进行增量收集。
返回:(final_text, complete_tool_calls)
"""
collected = {} # index -> tool_call 的累积拼接
final_text = []
for chunk in response_stream:
if chunk.has_tool_deltas():
# 逐增量拼接:id、函数名、参数 JSON 片段
merge_tool_delta(collected, chunk.tool_deltas)
if chunk.has_content():
final_text.append(chunk.content)
return join_text(final_text), finalize_tool_calls(collected)
为什么需要这个 :OpenAI 兼容的流式 API 会将 tool_calls 拆分为多个 delta 块发送------比如工具名称在一个 chunk,参数在下一个 chunk,长参数甚至被切分到几十个 chunk 中。_collect_tool_stream 按 index 字段将碎片重组为完整的 tool_call。这是一个容易踩坑的实现细节,但也是流式工具调用必不可少的环节。
3.3 非流式 vs 流式路径
系统同时支持两种调用路径:
tool_augmented_ask()
├── stream=False (默认)
│ └── llm.chat_completion() → 同步获取完整 tool_calls
│
└── stream=True (thinking 模式)
└── llm.chat_completion_stream() → _collect_tool_stream() → 组装完整 tool_calls
非流式路径简单直接,适合后台批量处理。流式路径虽然实现复杂,但能让用户在 LLM "思考"过程中看到实时输出(thinking token),提升交互体验。
四、工具执行器映射
系统通过 _TOOL_EXECUTORS 字典将工具名称映射到具体的执行函数,这是典型的策略模式:
python
# 伪代码: 工具名到执行函数的映射表
TOOL_EXECUTOR_REGISTRY = {
"search_knowledge": execute_knowledge_search, # 搜索产品知识库
"browse_index": execute_browse_index, # 浏览文档索引
"read_section": execute_read_section, # 读取指定章节
"search_memory": execute_memory_search, # 搜索历史问答
"web_search": execute_web_search, # 互联网搜索
"decompose_question": execute_decompose, # 拆解复杂问题
"compare_sections": execute_compare, # 对比两个章节
"polish_answer": execute_polish, # 润色客服回答
"list_documents": execute_list_docs, # 列出所有文档
"transfer_to_human": execute_transfer_to_human, # 转人工客服
}
# 统一接口:每个执行器接收关键字参数,返回字符串结果
# 新增工具只需:定义 TOOL_DEFINITIONS 中的 schema + 添加映射函数
每个执行器函数都遵循统一的签名模式------接收从 tool_call 参数 JSON 中解析出的关键字参数,返回字符串形式的执行结果。这种统一接口让新增工具变得非常简单:定义 TOOL_DEFINITIONS 中的 schema,然后在 _TOOL_EXECUTORS 中添加映射函数即可。
执行器内部实现示例
以 _execute_search_knowledge 为例,它实际上是系统检索管线的"代理人":
python
async def _execute_search_knowledge(query: str, system: str = "auto"):
"""执行知识库搜索"""
# 确定搜索范围
target_systems = _resolve_systems(system)
# 调用检索核心
results = await retriever.search(
query=query,
target_systems=target_systems,
top_k=3,
mode=current_config.RETRIEVE_MODE # navigate/index/direct
)
if not results:
return "知识库中未找到相关信息。"
# 格式化结果
return _format_search_results(results)
各执行器分工明确:
| 执行器 | 内部调用 | 返回值特点 |
|---|---|---|
_execute_search_knowledge |
retriever.search() | 格式化片段,标注来源章节 |
_execute_browse_index |
读取索引文件 | 层级化目录树 |
_execute_read_section |
原文定位器 | 完整章节原文 |
_execute_search_memory |
memory.search() | 匹配分数+评分+答案 |
_execute_web_search |
Tavily/Bocha API | 标题+摘要+URL |
_execute_decompose_question |
LLM 拆分 | JSON 子问题列表 |
_execute_compare_sections |
read_section × N + LLM 对比 | 结构化对比表格 |
_execute_polish_answer |
LLM 润色 | 客服风格文本 |
_execute_list_documents |
遍历索引目录 | 完整文档清单 |
_execute_transfer_to_human |
CSV 记录 + 返回消息 | 确认转人工消息 |
五、与策略路由的关系
系统的 TOOL_USE_STRATEGY 配置项决定了工具调用的上层策略:
TOOL_USE_STRATEGY = react → tool_augmented_ask() 直接 ReAct 循环
TOOL_USE_STRATEGY = auto → planner.py 策略路由 → 选择 A/B/C/D/skip
react 模式(默认)
直接使用 tools.py 的 tool_augmented_ask() 函数,LLM 在 ReAct 循环中自主选择 10 个工具。这是最简单直接的路径------LLM 既是规划者又是执行者,所有决策在循环中动态做出。
优点:实现简单,LLM 可以灵活应对各种意外情况,不需要预先规划。
缺点:每次工具调用都需要一次 LLM 推理,Token 消耗较大;LLM 可能"过度思考",调用不必要的工具。
auto 模式(策略路由)
通过 planner.py 先让 LLM 分析问题类型,路由到四种预定义策略之一:
| 策略 | 工具使用方式 | 典型场景 |
|---|---|---|
| A (ReAct) | 直接使用 tools.py 的 ReAct 循环 | 单步简单查询 |
| B (ReWOO) | 先规划工具序列,再按序执行 | 多独立子问题 |
| C (ReWOO+ReAct) | 规划大方向,每步灵活调整 | 跨章节有依赖的流程 |
| D (ReWOO+Reflexion) | 规划→执行→自检补漏 | 对比分析需完整性 |
四种策略共享同一个_TOOL_EXECUTORS 映射**和工具定义,区别在于执行编排方式:
- 策略 A 直接调用
tool_augmented_ask(),走 ReAct 循环。 - 策略 B/C/D 自行编排工具调用序列(先规划再执行),但仍然使用
_TOOL_EXECUTORS中的函数作为执行单元。
这体现了分层设计的思想:工具执行(如何做)与策略编排(何时做)是分离的,策略路由只决定"调用顺序",工具模块只负责"执行能力"。
质量门控的衔接
无论是 react 还是 auto 模式,最终的输出都会经过质量门控(QUALITY_GATE):
tool_augmented_ask() 输出 → QUALITY_GATE 评分
├── score >= 8 → 直接返回给用户
├── 5 <= score < 8 → 补搜一轮 → 再评估
└── score < 5 → 回退到 ReAct(带已有结果上下文)
质量门控与工具系统的衔接之处在于:补搜时仍使用同一个工具集。即使未通过质量检查,已经执行过的工具结果也不会浪费------它们作为上下文保留在消息序列中,补搜轮次的 LLM 可以看到之前的搜索结果,站在已有信息的基础上继续探索。
六、并发安全设计
在生产环境中,一个 Web 服务可能同时处理多个用户的问答请求。如果多个请求共享同一个工具执行上下文,就会出现数据污染 ------用户 A 的工具调用结果被用户 B 看到。为此,系统使用 contextvars.ContextVar 实现请求级隔离:
python
# 伪代码: 使用 ContextVar 实现请求级隔离
from contextvars import ContextVar
# 每个请求独立的上下文
_current_context: contextvars.ContextVar = contextvars.ContextVar(
'tool_context', default=None
)
async def tool_augmented_ask(question, context, ...):
# 设置当前请求的上下文
token = _current_context.set(context)
try:
# ReAct 循环中的每一步都能通过 _current_context.get()
# 获取当前请求隔离的上下文数据
async for result in _react_loop(question):
yield result
finally:
# 请求结束,恢复之前的上下文
_current_context.reset(token)
ContextVar 的三大作用
-
请求级隔离 :每个请求的
_current_context包含独立的对话历史、会话 ID、配置快照。同时处理的 100 个请求不会互相干扰。 -
执行器参数传递 :工具执行器函数通过
_current_context.get()获取当前请求的配置(如检索模式、LLM 提供商选择),无需在每层函数调用中显式传递context参数:
python
async def _execute_search_knowledge(query: str, system: str = "auto"):
ctx = _current_context.get()
retriever_mode = ctx.get("retrieve_mode", "navigate")
# 使用 ctx 中的配置执行检索
- 异步安全 :
contextvars是 Python 官方推荐的异步上下文隔离方案。与threading.local不同,ContextVar在async/await切换时能正确保持隔离------即使await导致事件循环切换到另一个协程,每个协程的ContextVar值仍然是独立的。
为什么不用显式参数传递?
一种替代方案是将 context 作为参数层层传递到每个执行器函数。但在 10 个工具的场景下,这意味着每个执行器函数都需要增加 context 参数,且调用链中的所有中间函数都需要透传。ContextVar 将上下文从"显式传参"变为"隐式获取",减少了函数签名污染,降低了新增工具时的改动成本。
七、TOOL_USE_SYSTEM_PROMPT 的设计
为了让 LLM 正确选择工具,系统设计了精密的 TOOL_USE_SYSTEM_PROMPT。其核心指导原则包括:
工具选择优先级
1. 优先使用 search_knowledge 搜索具体问题(覆盖面最广)
2. 结构类问题("有哪些模块")使用 browse_index
3. search_knowledge 结果不够详细时,用 read_section 读原文
4. 对比类问题先用 search_knowledge 分别搜索,再用 compare_sections
5. 复杂多意图问题先用 decompose_question 拆分
6. 高频重复问题优先查 search_memory(Token 成本最低)
7. 知识库无结果时,最后尝试 web_search
8. 最终输出前调用 polish_answer 润色
9. 以上均不可解决时,调用 transfer_to_human
规则约束
- 禁止连续调用同一工具 :如果第一次
search_knowledge没找到结果,第二次应该用browse_index或read_section换角度尝试,而非相同关键词再搜一次。 - 一次最多调用 3 个工具:单轮 ReAct 循环中,LLM 可以调用多个并行的 tool_calls,但不超过 3 个,避免一次性发太多请求。
- 不要过度分解 :只有确实包含多个独立子问题时才调用
decompose_question,不要为简单问题增加不必要的中转。 - 确保最终输出包含来源:无论是直接回答还是润色后的答案,都应该标注知识来源章节。
工具选择的动态性
TOOL_USE_SYSTEM_PROMPT 不是静态的------系统会根据当前配置动态调整提示词内容。例如,当 WEB_SEARCH_ENABLED=false 时,web_search 的描述会被修改为"已禁用";当 MEMORY_ENABLED=false 时,search_memory 被移除出可用工具列表。
八、工程实践中的关键优化
8.1 并行工具调用
OpenAI Function Calling 支持单次响应中返回多个 tool_calls。系统利用这一特性:
python
# LLM 在一次推理中决定同时调用两个工具
# 这在 compare_sections 场景下尤其有用
tool_1 = search_knowledge(query="系统X审批流程", system="系统X")
tool_2 = search_knowledge(query="系统B审批流程", system="系统B")
# 两个工具并行执行
results = await asyncio.gather(
_TOOL_EXECUTORS["search_knowledge"](**tool_1.args),
_TOOL_EXECUTORS["search_knowledge"](**tool_2.args),
)
效果 :LLM 可以在同一轮中发起多个并行的工具调用(如同时搜索两个业务系统),asyncio.gather 让它们并发执行,显著减少总延迟。
8.2 工具结果缓存
在同一个会话的同一轮 ReAct 循环中,如果 LLM 不小心重复调用了同一参数的工具,系统会检查缓存:
python
_tool_cache: dict = {}
async def _execute_with_cache(tool_name, args):
cache_key = f"{tool_name}:{json.dumps(args, sort_keys=True)}"
if cache_key in _tool_cache:
return _tool_cache[cache_key] # 返回缓存结果
result = await _TOOL_EXECUTORS[tool_name](**args)
_tool_cache[cache_key] = result
return result
这在 LLM 的"探索性行为"中很常见------有时候它不确信之前的搜索结果是否完整,会尝试用相似的参数再搜一次。缓存避免了不必要的重复检索。
8.3 工具超时保护
每个工具执行都有独立的超时限制(默认 15 秒),防止某个工具(尤其是 web_search 这种外部 API 调用)卡死整个 ReAct 循环:
python
async def _execute_with_timeout(executor, **kwargs):
try:
return await asyncio.wait_for(
executor(**kwargs),
timeout=TOOL_TIMEOUT_SECONDS
)
except asyncio.TimeoutError:
return f"工具执行超时({TOOL_TIMEOUT_SECONDS}秒),请重试或换用其他工具。"
九、总结与经验
架构全景图
┌─────────────────┐
│ TOOL_USE_STRATEGY │
│ = react / auto │
└────────┬────────┘
│
┌──────────────┴──────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ReAct │ │ ReWOO │ │Reflexion │
│ 循环 │ │ 规划执行 │ │ 自检补漏 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼─────────────┘
│
▼
┌─────────────────────┐
│ _TOOL_EXECUTORS │
│ (10 个工具执行器) │
├─────────────────────┤
│ search_knowledge │
│ browse_index │
│ read_section │
│ search_memory │
│ web_search │
│ decompose_question │
│ compare_sections │
│ polish_answer │
│ list_documents │
│ transfer_to_human │
└─────────────────────┘
│
┌────────────┴────────────┐
│ ContextVar 并发隔离 │
│ 请求 A │ 请求 B │ 请求 C │
└─────────────────────────┘
关键设计原则
-
工具即能力抽象:每个工具封装一项原子能力,LLM 通过自然语言推断调用哪个工具。工具的粒度设计遵循"够用但不冗余"原则------10 个工具覆盖了问答全流程。
-
统一执行接口 :
_TOOL_EXECUTORS映射 + 统一签名模式,让新增工具的成本降到最低。只需要定义 TOOL_DEFINITIONS 和实现执行函数,两步即可完成。 -
循环而非流水线 :ReAct 循环的核心优势在于反馈驱动------每一步的工具结果都是下一步决策的依据。这与固定流水线不同,LLM 可以根据"观察"到的结果动态调整策略。
-
并发安全从设计第一天考虑:ContextVar 的方案比事后加锁优雅得多,它让工具执行器函数彻底摆脱了对请求上下文参数的依赖,同时也完全避免了并发污染。
-
分层编排 vs 自主任意 :
react模式让 LLM 自由选择工具(灵活),auto模式通过策略路由约束工具编排(可控)。两者共享相同的工具执行器,体现了"能力与策略分离"的架构思想。
适用场景
这套 Tool-Augmented Agent 设计虽然源于某个 FAQ,但其架构模式具有通用性:
- 企业内部知识库 QA:产品手册、操作指南、FAQ 的智能问答
- SaaS 产品内嵌帮助:用户在产品界面中直接提问,Agent 调用多个工具检索答案
- 客服辅助系统:人工客服的 AI 搭档,自动搜索知识库、对比文档、润色回复
- 技术文档问答:SDK 文档、API 文档的多工具交互式问答
如果你正在构建一个需要自主调用多个工具的 Agent 系统,本文的设计思路------工具即能力、循环即策略、ContextVar 保安全------应该能为你提供一些可落地的参考。
本系列文章索引:
- 01: Agent 记忆分层设计实践
- 02: 多策略路由的智能客服系统设计
- 03: 五种检索引擎与混合匹配架构
- 04: Agentic RAG 质量自检与自适应重试
- 05: 多意图查询拆分与递归分解
- 06: 配置驱动架构与双模式管线
- 07: LLM 基础设施层与提供商抽象
- 08: 文档索引构建:代码提取加 LLM 填充的混合架构
- 09: Tool-Augmented Agent 工具调用设计(本文)