Skip to content

生成式 UI ​

模型不仅能用文本回答,还能用 组件 回答。当它调用某个工具时,<MessageParts> 会渲染你为该工具映射的组件。组件会在参数流式传输时接收参数,随后接收结果。它还可以在对话继续之前请用户做出决定。

该演示使用了一个脚本化的模型,因此可以即时运行,无需网络。下面的代码与使用真实提供方时完全相同。

1. 定义工具 ​

ts
import { defineTool } from 'vue-llm'

export const getWeather = defineTool<{ city: string; days: number }, Weather>({
  name: 'get_weather',
  description: 'Current weather and forecast for a city',
  parameters: {
    type: 'object',
    properties: { city: { type: 'string' }, days: { type: 'number' } },
    required: ['city'],
  },
  execute: ({ city, days }, { signal }) =>
    fetch(`/api/weather?city=${city}&days=${days}`, { signal }).then(r => r.json()),
})

// No execute: the call waits until the user answers in the UI
export const confirmBooking = defineTool<Booking, Booking & { confirmed: boolean }>({
  name: 'confirm_booking',
  description: 'Ask the user to confirm a restaurant booking',
  parameters: { /* … */ },
})

2. 为每个工具编写组件 ​

工具组件接收 ToolComponentProps<Args, Result>:

Prop说明
args参数,在 state 为 'streaming' 时是不完整的
statestreaming → pending → running → result / error
result工具的返回值
error工具抛出错误时的错误信息
addResult(result)从 UI 中完成该调用
toolCallId、toolName标识符
vue
<!-- WeatherCard.vue -->
<script setup lang="ts">
import type { ToolComponentProps } from 'vue-llm'

const props = defineProps<ToolComponentProps<{ city: string; days: number }, Weather>>()
</script>

<template>
  <div class="card">
    <h3>{{ props.args?.city ?? '…' }}</h3>
    <Forecast v-if="props.result" :weather="props.result" />
    <Skeleton v-else />
  </div>
</template>
vue
<!-- BookingCard.vue: human in the loop -->
<script setup lang="ts">
import type { ToolComponentProps } from 'vue-llm'

const props = defineProps<ToolComponentProps<Booking, Booking & { confirmed: boolean }>>()
const decide = (confirmed: boolean) => props.addResult({ ...props.args as Booking, confirmed })
</script>

<template>
  <div class="card">
    {{ props.args?.restaurant }} at {{ props.args?.time }}
    <template v-if="props.state === 'pending'">
      <button @click="decide(true)">Confirm</button>
      <button @click="decide(false)">Cancel</button>
    </template>
  </div>
</template>

3. 渲染消息 ​

vue
<script setup lang="ts">
import { useChat } from 'vue-llm'
import { MessageParts } from 'vue-llm/components'
import 'vue-llm/style.css'

const { messages, input, handleSubmit, addToolResult } = useChat({
  provider: ai,
  model: 'gpt-4o',
  tools: [getWeather, confirmBooking],
})

const components = { get_weather: WeatherCard, confirm_booking: BookingCard }
</script>

<template>
  <MessageParts
    v-for="message in messages"
    :key="message.id"
    :message="message"
    :components="components"
    @tool-result="addToolResult"
  />
</template>

这就是全部配置。get_weather 会自行运行,结果返回后模型继续生成。confirm_booking 会让聊天在 pending 状态下暂停。当用户点击 Confirm 时,addToolResult 会记录这个回答,聊天随之继续。参阅 人工介入。

不使用 MessageParts ​

MessageParts 是可选的。工具调用只是普通的消息 part,因此你可以自己渲染它们:

vue
<template v-for="part in message.parts">
  <WeatherCard
    v-if="part.type === 'tool-call' && part.toolName === 'get_weather'"
    v-bind="part"
    :add-result="(result) => addToolResult({ toolCallId: part.toolCallId, result })"
  />
</template>

这种方式同样适用于无法使用 vue-llm/components 的 Vue 2.7。

流式参数 ​

参数是逐 token 到达的。vue-llm 会在每个 chunk 到达时用 parsePartialJSON(同样对外导出)解析不完整的 JSON,因此在模型写完整个调用之前,args.city 就已经出现了。字段会在完整后被加入;字符串会原地增长,而不完整的键在完成之前会被丢弃。

ts
import { parsePartialJSON } from 'vue-llm'

parsePartialJSON('{"city":"Os')           // { city: 'Os' }
parsePartialJSON('{"items":[{"n":"a"},{') // { items: [{ n: 'a' }, {}] }

服务端工具 ​

需要密钥的工具应放在服务端(createLLMHandler 工具)。它们的调用仍会以 state: 'result' 的 tool-call part 流式传输到客户端,因此同样的组件也能渲染它们。

基于 MIT 许可发布。