在做 AI 对话平台时,我们遇到一个需求:让 AI 能调用浏览器端的能力(地理位置、文件读取、语音输入等)。这些能力后端没有,只能在前端执行。
但现有的 function calling 方案都是后端闭环的。于是需要设计一套前端工具调用机制,跑在 SSE 流式场景下。
本文记录实现过程中遇到的几个关键问题和解法。
问题1:arguments 是增量到达的
和普通的文本 token 流式输出一样,模型输出 tool_call 时 arguments 字段也是分片到达的:
event: tool_call
data: {"name": "get_location", "arguments": "{\"ac"}
event: tool_call
data: {"name": "get_location", "arguments": "cura"}
event: tool_call
data: {"name": "get_location", "arguments": "cy\": \"high\"}"}前端需要把这些片段拼成完整的 JSON 字符串,再 JSON.parse 得到参数对象,然后才能执行对应工具。
实现上用一个 Map 按 tool_call_id 做累积:
const pendingArgs = useRef<Map<string, string>>(new Map());
function handleToolCallDelta(id: string, argsDelta: string) {
const current = pendingArgs.current.get(id) || '';
pendingArgs.current.set(id, current + argsDelta);
}
function handleToolCallComplete(id: string, toolName: string) {
const argsJson = pendingArgs.current.get(id);
const args = JSON.parse(argsJson);
executeFrontendTool(toolName, args);
pendingArgs.current.delete(id);
}问题2:执行结果怎么回传
前端工具执行完后,结果要送回后端,让模型继续生成。
后端提供一个 POST /chat/tool-result 接口:
async function submitToolResult(chatId: string, toolCallId: string, result: any) {
await fetch('/api/chat/tool-result', {
method: 'POST',
body: JSON.stringify({ chatId, toolCallId, result: JSON.stringify(result) })
});
}后端收到后把 tool result 塞进消息列表,解锁阻塞的生成流程。
这里有个时序问题:后端发出 tool_call 事件后会进入等待状态(不继续生成),直到收到前端回传的结果。如果前端执行失败或超时,需要有兜底机制。
问题3:React StrictMode 下的重复执行
这个坑比较隐蔽。
React 18+ 的 StrictMode 在开发环境下会让 useEffect 执行两次。对于 SSE 连接来说,这意味着同一个 tool_call 事件会被处理两次,同一个前端工具会被执行两次。
如果工具有副作用(比如弹出文件选择框),用户会看到弹了两次。
解决方案是加一个 ref 做防重入守卫:
const processedToolCalls = useRef<Set<string>>(new Set());
function handleToolCall(toolCallId: string, toolName: string, args: any) {
if (processedToolCalls.current.has(toolCallId)) return;
processedToolCalls.current.add(toolCallId);
executeFrontendTool(toolName, args);
}问题4:多工具并行
模型可能一次输出多个 tool_call(比如同时要地理位置和搜索结果)。
三条轨道(后端/前端/代码)互不阻塞,前端内部多个工具也要并行执行。用 Promise.allSettled 做并发管理,确保单个工具失败不影响其他工具:
const results = await Promise.allSettled(
frontendToolCalls.map(tc => executeFrontendTool(tc.name, tc.args))
);以上是我们在实际项目中的实践记录,希望对同样在做 SSE 流式工具调用的同学有参考价值。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。