工具扩展:工具插件系统深度剖析
面向“可插拔、可并发、可流式”的工具系统。以统一的处理器接口和事件协议为核心,结合自动发现与上下文注入,支撑从内置工具到自定义定义类(definition-only)的多形态工具。
设计目标与原则
- 统一接口:对内以
ToolHandler为契约,对外兼容 Assistant/Responses 的工具协议与流式事件。 - 插件化发现:基于 Spring 扫描与注册,零侵入纳管新工具实现。
- 并发安全:工具并发执行,输出串行呈现,避免多路输出的竞争与乱序。
- 按需执行:支持“定义类工具(definition-only)”——服务端不执行,仅发出调用事件并进入
requires_action。 - 多模态友好:统一文件/资源注入与隐藏策略,使图像、检索等工具“即插即用”。
架构总览
- 发现:
ToolFetcher启动时扫描 Spring 容器中的ToolHandlerBean,注册到内建表toolName → handler。- 代码:
api/src/main/java/com/ke/assistant/core/tools/ToolFetcher.java
- 代码:
- 执行:
RunExecutor创建的ToolExecutor负责并发执行工具、处理返回、生成或透传流式事件。- 代码:
api/src/main/java/com/ke/assistant/core/tools/ToolExecutor.java
- 代码:
- 输出:
ToolOutputChannel以“单说话人”策略串行发送工具输出,ResponseMessageExecutor转译为 Responses SSE。- 代码:
api/src/main/java/com/ke/assistant/core/tools/ToolOutputChannel.java - 代码:
api/src/main/java/com/ke/assistant/core/run/ResponseMessageExecutor.java
- 代码:
核心契约:ToolHandler
-
接口:
api/src/main/java/com/ke/assistant/core/tools/ToolHandler.javaToolResult execute(ToolContext, Map<String,Object>, ToolOutputChannel)String getToolName():工具名称,也是注册键与 LLM 工具名。Map<String,Object> getParameters()/getDescription():用于构建函数定义(见ToolFetcher.fetchChatTool(...))。boolean isFinal():是否为“终结工具”,输出可 直接成为助手消息内容。default boolean isDefinitionHandler():是否为“定义类工具”。
-
上下文:
ToolContext注入工具定义与执行态- 代码:
api/src/main/java/com/ke/assistant/core/tools/ToolContext.java - 字段:
tool、toolId、files(ToolFiles映射)、user、bellaContext。
- 代码:
-
结果:
ToolResult统一承载文本/图片/链接等结果,带mime_type等元数据- 代码:
api/src/main/java/com/ke/assistant/core/tools/ToolResult.java
- 代码:
发现与注册:ToolFetcher
- 启动扫描:
@PostConstruct initialize()→searchAndRegisterTools()- 从 Spring 容器拉取所有
ToolHandlerBean,按getToolName()注册。 - 代码:
ToolFetcher#getToolHandler(String)、getToolHandler(Tool) - 自定义工具(
Tool.Custom)会包装成CustomToolHandler。
- 从 Spring 容器拉取所有
- 函数定义生成:
fetchChatTool(toolName)将ToolHandler的描述/参数转换为 ChatCompletion 的function定义。
执行与并发:ToolExecutor
-
启动与注册:
ToolExecutor.start(...)- 从
ExecutionContext.getTools()读取工具定义,过滤掉type=function(函数交由 LLM/客户端),其它注册为内部工具。
- 从
-
主循环:
loop()- 等待
context.toolCallAwait(),批量获取ChatToolCall任务。 - 为每个任务构造
ToolContext并并发执行(TaskExecutor.supplyCaller)。 - 条件开启
ToolOutputChannel:context.isResponseApi()或handler.isFinal()。 - 处理结果:
processResult(...)将结构化结果回填到ToolCall,交由RunStateManager.finishToolCall(...)。 - 定义类工具:
handler.isDefinitionHandler()==true→ 汇总到requiredTools,最终触发RequiredAction: submit_tool_outputs。
- 等待
-
关键代码:
api/src/main/java/com/ke/assistant/core/tools/ToolExecutor.javaprocessResult(...)支持code_interpreter/file_search/function的结果回填。
流式输出与串行化:ToolOutputChannel
- 目标:工具并发执行但输出串行呈现。
- 策略:按
toolCallId分桶缓存;若ExecutionContext.currentOutputToolCallId已设置,仅发送该桶;否则随机挑选桶并设置当前输出者。 - 接口:
output(toolCallId, Object):发送通用内容(例如文本)output(toolCallId, Tool, Object):带工具定义 ,用于忽略hidden()工具。
- 代码:
api/src/main/java/com/ke/assistant/core/tools/ToolOutputChannel.java
事件协议:ToolStreamEvent → Responses SSE
- 事件载体:
ToolStreamEvent{ toolCallId, BaseStreamEvent event, ExecutionStage, result }- 代码:
api/src/main/java/com/ke/assistant/core/tools/ToolStreamEvent.java
- 代码:
- 典型事件:
- 图像生成:
ImageGenerationInProgress/Generating/PartialImage/Completed - 文件检索:
FileSearchInProgress/Searching/Completed - 自定义输入:
CustomToolCallInputDelta/InputDone - 本地 Shell:
OutputItemAdded/OutputItemDone
- 图像生成:
- 转译器:
ResponseMessageExecutor.handleToolStreamEvent(...)将事件封装为 Responses SSE,自动生成item_id、维护sequenceNumber与outputIndex,并在工具完成时finishToolCallOutput()。
定义类工具(Definition-only)
- 契约:
ToolDefinitionHandler(继承ToolHandler)- 服务端不真正执行,仅按协议发送调用事件;随后进入
requires_action,等待客户端submit_tool_outputs
- 服务端不真正执行,仅按协议发送调用事件;随后进入