数据截 至 (上游 commit ceade4cbe9f2)
工具执行、RAG/file search、MCP,与后台响应/自动压缩
30 秒导读: 上一章(04)讲的 agentic 主循环,决定"要不要再调一次模型、模型这轮想调哪些工具"。但模型只会说"我要调
file_search(query=...)",它自己不会去搜。这一章讲那句话怎么被真正执行——内置 RAG(接向量库)、web 搜索、以及外部 MCP 工具怎么被发现、批准、调用;再讲两个让系统能扛"长任务、长对话"的高级机制:后台响应(请求立即返回、干活在后台队列里跑)和自动上下文压缩(对话太长就先摘要,省 token)。
1. 这章在整幅图里的位置
先用一句话把边界划清:04 讲"决策",本章讲"执行 + 两个扩展机制"。
主循环每转一圈,模型的输出里可能夹着若干"工具调用"(tool call)。04 的编排器负责把它们分类、排序、决定循环是否继续;真正"把工具跑起来、把结果塞回对话"的脏活,交给本章的两个主角:
| 角色 | 文件 | 职责一句话 |
|---|---|---|
ToolExecutor | providers/inline/responses/builtin/responses/tool_executor.py | 拿到一个 tool call,分派到 RAG/web/MCP/函数,执行,发流式进度,产出结果消息 |
| 编排 器的 MCP 方法 | providers/inline/responses/builtin/responses/streaming.py | 发现 MCP 工具、缓存工具清单、把"需人工批准"的工具挡在门外 |
MCPSessionManager | providers/utils/tools/mcp.py | 一次请求内复用 MCP 连接,避免每次调用都重连、重列工具 |
| 后台执行 + 自动压缩 | providers/inline/responses/builtin/responses/openai_responses.py | 长任务丢队列后台跑;长对话超阈值先摘要 |
本章不重复 04 的主循环骨架;需要主循环上下文时请回看 04。
2. 顶层全景:一次工具调用怎么落地
先看"决策 → 执行"这条缝在哪儿对上。下图从左到右是一次工具调用的生命周期,编排器负责左半(决定),ToolExecutor 负责右半(执行):
编排器主循环(04) ToolExecutor(本章)
┌───────────────────────────┐ ┌────── ────────────────────────┐
│ 模型返回 tool_calls │ │ execute_tool_call │
│ │ │ ① 发"开始"进度事件 │
│ _separate_tool_calls │ │ ② _execute_tool 分派执行 │
│ ├ 函数工具 → 交回客户端 │ │ ├ file_search → 向量库 │
│ ├ 内置/MCP → 服务端执行 │──每个──▶ │ ├ web_search → 工具运行时│
│ └ 需批准? → 挡下, 发批准请求│ tool_call │ └ MCP 名 → invoke_mcp │
└───────────────────────────┘ │ ③ 发"完成/失败"进度事件 │
▲ │ ④ _build_result_messages │
│ 把工具结果塞回 next_turn │ → 输出消息 + 喂回模型的消息│
└────────────────────────────────│ ⑤ yield 最 终结果 │
└──────────────────────────────┘
怎么读:一个 tool call 进 execute_tool_call,走 ①→⑤ 五步;第 ⑤ 步产出的"喂回模型的消息"会被编排器 append 进 next_turn_messages,于是下一轮模型就"看见"了工具结果。这正是 agentic 循环能自我推进的燃料。
入口签名在 tool_executor.py:135(ToolExecutor.execute_tool_call),它是个异步生成器——一边执行一边 yield 进度事件,最后 yield 带结果的 ToolExecutionResult。
3. ToolExecutor:工具调用怎么被真正执行
3.1 五步骨架
execute_tool_call(tool_executor.py:135)本身很短,像一条流水线,把活分给四个私有方法:
# 示意,非源码。重点看"一次调用被切成五步"
async def execute_tool_call(tool_call, ctx, ...):
kwargs = json.loads(tool_call.function.arguments) # 模型给的参数是 JSON 字符串
# ① 发"开始"事件(不同工具类型发不同事件)
async for ev in self._emit_progress_events(name, ...): yield ev
# ② 真正执行(唯一会失败的地方,错误被收进 error_exc)
error_exc, result = await self._execute_tool(name, kwargs, ctx, mcp_map)
# ③ 发"完成/失败"事件
async for ev in self._emit_completion_events(name, ..., has_error): yield ev
# ④ 把结果拼成两条消息(见 3.5)
out_msg, in_msg = await self._build_result_messages(...)
# ⑤ 交回最终结果 + 引用文件
yield ToolExecutionResult(final_output_message=out_msg, final_input_message=in_msg, ...)
一个关键设计:执行只发生在第 ② 步,且被 try/except 兜住——_execute_tool 从不抛异常给上层,而是返回 (error_exc, result) 二元组(tool_executor.py:383)。于是"工具挂了"不会掀翻整个响应流,只会变成一条 status="failed" 的输出消息。容错的根就扎在这里。
3.2 _execute_tool:一张分派表
_execute_tool(tool_executor.py:383)按工具名把调用路由到不同后端。看清这张表,就看清了 OGX 支持哪几类工具:
| 判定条件 | 走哪条路 | 落到哪个 API |
|---|---|---|
工具名在 mcp_tool_to_server 里 | invoke_mcp_tool(...) | 外部 MCP 服务器(见 §4) |
工具名是 knowledge_search / file_search | _execute_file_search_via_vector_store | 内置 RAG → VectorIO(见 §3.3) |
工具名是 web_search | tool_runtime_api.invoke_tool | 工具运行时 provider |
| 其它(兜底) | tool_runtime_api.invoke_tool | 工具运行时 provider |
判定顺序很重要:MCP 优先(tool_executor.py:395),因为 MCP 工具名是运行时动态注册的,不能和内置名撞车。web_search 分支还会把 response 级配置(allowed_domains、user_location、search_context_size)从 ctx.response_tools 抠出来塞进 kwargs(tool_executor.py:436-450),这样调用方在工具定义上写的过滤条件才真正生效。
每条真执行路径外面都裹了一层 OpenTelemetry span(如 tracer.start_as_current_span("invoke_mcp_tool", ...),tool_executor.py:408),用于分布式追踪工具耗时。
3.3 内置 RAG:file search 怎么接向量库
这是本章工程含量最高的一块。file_search / knowledge_search 不是简单转发,而是在服务端跑一遍完整的检索 + 组装 RAG 上下文。核心在 _execute_file_search_via_vector_store(tool_executor.py:194)。
它要解决的小问题: 模型说"帮我搜 X",系统得:在多个向量库里并行检索 → 把命中的 chunk 拼成一段带引用标注的文本 → 让模型能在回答里正确 cite 来源文件。
思路,分四步走:
① 并行检索:对每个 vector_store_id 起一个 search 任务
search_single_store(vid) ──┐
search_single_store(vid) ──┼─▶ asyncio.gather ─▶ 扁平化所有命中
search_single_store(vid) ──┘
② 拼装:header 模板 + 每个 chunk 套 annotation 模板 + footer 模板
③ 收集引用:{file_id → filename} 映射(citation_files)
④ 打包成 ToolInvocationResult(content=文本块, metadata=检索明细)
几个值得记住的细节:
- 并行 + 单库容错:每个库的检索包在自己的 try/except 里,某个库挂了只
logger.warning并返回空列表(tool_executor.py:219-221),不拖垮其它库。多库检索用asyncio.gather一把并发(tool_executor.py:225)。 - 检索走的是向量库的 search API,不是底层 chunk 接口:
self.vector_io_api.openai_search_vector_store(...)(tool_executor.py:207),这样才能支持filters和ranking_options。检索模式取自配置chunk_retrieval_params.default_search_mode。 - RAG 提示词全是模板化的:header/footer/context/annotation 四类模板都来自
VectorStoresConfig,注释开关关掉时回落到默认模板(tool_executor.py:241-250)。这让平台方能不改代码就定制"