工具调用面板
AI 不只是聊天——它还能调用工具。这节实现 function call 的可视化展示,让用户看到 AI "做了什么"。
什么是工具调用
现代 AI 模型支持 function calling:AI 在对话过程中可以调用预定义的工具函数。比如用户问"北京天气怎么样",AI 会调用 get_weather 函数获取数据,然后把结果组织成自然语言回复。
在聊天界面中,用户应该能看到 AI 调用了什么工具、传了什么参数、返回了什么结果。这就是工具调用面板。
类型设计
扩展 StreamChunk 和 Message 类型:
// packages/core/src/types/index.ts 扩展
export interface ToolCall {
id: string
name: string
arguments: string // JSON string
result?: string // 工具返回的结果
status: 'calling' | 'completed' | 'error'
error?: string
duration?: number // 耗时(ms)
}
// StreamChunk 新增 tool_call 类型
export type StreamChunk =
| { type: 'text'; content: string }
| { type: 'tool_call'; toolCall: ToolCall }
| { type: 'tool_result'; toolCallId: string; result: string }
| { type: 'done'; messageId: string }
// Message 新增 toolCalls 字段
export interface Message {
id: string
role: 'user' | 'assistant' | 'system'
content: string
attachments?: Attachment[]
toolCalls?: ToolCall[] // 新增
metadata?: Record<string, unknown>
}
typescript
ToolCallPanel 组件
<!-- packages/vue/src/components/ToolCallPanel.vue -->
<template>
<div :class="['ai-chat__tool-call', `ai-chat__tool-call--${toolCall.status}`]">
<!-- 头部:工具名 + 状态 -->
<div class="ai-chat__tool-call__header" @click="expanded = !expanded">
<span class="ai-chat__tool-call__icon">
{{ statusIcon }}
</span>
<span class="ai-chat__tool-call__name">{{ toolCall.name }}</span>
<span v-if="toolCall.duration" class="ai-chat__tool-call__duration">
{{ toolCall.duration }}ms
</span>
<span class="ai-chat__tool-call__toggle">
{{ expanded ? '收起' : '展开' }}
</span>
</div>
<!-- 展开详情 -->
<div v-if="expanded" class="ai-chat__tool-call__detail">
<!-- 参数 -->
<div class="ai-chat__tool-call__section">
<div class="ai-chat__tool-call__label">参数</div>
<pre class="ai-chat__tool-call__code">{{ formattedArgs }}</pre>
</div>
<!-- 结果 -->
<div v-if="toolCall.result" class="ai-chat__tool-call__section">
<div class="ai-chat__tool-call__label">返回结果</div>
<pre class="ai-chat__tool-call__code">{{ formattedResult }}</pre>
</div>
<!-- 错误 -->
<div v-if="toolCall.error" class="ai-chat__tool-call__error">
{{ toolCall.error }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import type { ToolCall } from '@ai-chat/core'
const props = defineProps<{ toolCall: ToolCall }>()
const expanded = ref(false)
const statusIcon = computed(() => {
switch (props.toolCall.status) {
case 'calling': return '⏳'
case 'completed': return '✅'
case 'error': return '❌'
}
})
const formattedArgs = computed(() => {
try {
return JSON.stringify(JSON.parse(props.toolCall.arguments), null, 2)
} catch {
return props.toolCall.arguments
}
})
const formattedResult = computed(() => {
try {
return JSON.stringify(JSON.parse(props.toolCall.result!), null, 2)
} catch {
return props.toolCall.result
}
})
</script>
vue
样式设计
.ai-chat__tool-call {
background: var(--ai-chat-surface);
border: 1px solid var(--ai-chat-border-color);
border-radius: var(--ai-chat-radius-md);
margin: var(--ai-chat-spacing-xs) 0;
font-size: 13px;
}
.ai-chat__tool-call--calling {
border-color: #F59E0B;
}
.ai-chat__tool-call--error {
border-color: #EF4444;
}
.ai-chat__tool-call__header {
display: flex;
align-items: center;
gap: var(--ai-chat-spacing-xs);
padding: 8px 12px;
cursor: pointer;
}
.ai-chat__tool-call__code {
background: var(--ai-chat-code-bg);
color: #ABB2BF;
padding: 8px 12px;
border-radius: 4px;
font-family: Consolas, monospace;
font-size: 12px;
overflow-x: auto;
}
css
集成到消息流
修改 useChat 消费工具调用 chunk:
// packages/core/src/composables/useChat.ts 扩展
for await (const chunk of stream) {
switch (chunk.type) {
case 'text':
assistantMessage.content += chunk.content
break
case 'tool_call':
if (!assistantMessage.toolCalls) assistantMessage.toolCalls = []
assistantMessage.toolCalls.push(chunk.toolCall)
break
case 'tool_result': {
const tc = assistantMessage.toolCalls?.find(t => t.id === chunk.toolCallId)
if (tc) {
tc.result = chunk.result
tc.status = 'completed'
}
break
}
}
}
typescript
MessageBubble 中渲染工具调用:
<!-- MessageBubble 中新增 -->
<ToolCallPanel
v-for="tc in message.toolCalls"
:key="tc.id"
:tool-call="tc"
/>
vue
用AI生成工具调用组件
基于 ai-chat-ui 的现有组件结构,实现 ToolCallPanel 组件。
要求:
1. 展示工具名、调用状态(calling/completed/error)、耗时
2. 可折叠展开查看参数和返回结果
3. 参数和结果用 JSON 格式化显示
4. 样式使用组件库的 Design Token 变量
5. 支持 StreamChunk 中的 tool_call 和 tool_result 两种类型
text
mock 剧本里的工具调用回放
多会话 mock 服务(下一节)为工具调用可视化提供了数据源。dev-server 的剧本把一次工具调用编排为两个原子事件:tool_call 帧(metadata 携带 toolCallId/toolName/toolArguments)停顿约 500ms 后,tool_result 帧(同 toolCallId 回填结果)再停约 900ms,最后才输出正文——前端能真实看到"调用中→已完成"的状态流转,而不是一次性拿到终态。组件联调时这类时间编排的假数据比静态 JSON 有用得多,因为它逼出了流式状态机的每个分支。
类型层面的后续演进(真实仓库现状):status 已从本文的三值扩展为对齐 AI SDK 的七态——pending(参数流式组装中)/ calling / awaiting-approval(等待人工审批)/ completed / denied / error,arguments 从 JSON string 收紧为 Record<string, unknown>,组件更名为 tool-call/ToolCall 系。三值是七态的子集,语义不变。
小结
- 扩展 StreamChunk 支持
tool_call和tool_result两种新类型 - ToolCallPanel 组件:可折叠面板展示工具名、参数、结果、耗时
- 三种状态样式:calling(黄色边框)、completed(默认)、error(红色边框)
- useChat 在消费流时处理工具调用 chunk,更新消息的 toolCalls 数组
- JSON 参数和结果自动格式化,方便调试
- mock 剧本按时间编排 tool_call→tool_result 事件,让状态流转可被真实触发
下一节实现多会话管理——让用户同时维护多个对话。
↑