结构化数据抽取
Stream UI 不仅仅是一个渲染引擎,它还可以帮助你从流式文本中提取结构化的状态信息。通过使用 v-model:data,你可以实时获取当前已解析出的所有区块(Blocks)的状态。
为什么需要它?
在渲染流式对话时,除了将内容显示在屏幕上,你可能还需要:
- 在外部 UI 中显示当前是否有“思考中”的状态。
- 统计当前输出中的所有代码块或自定义标签。
- 将解析后的结构化数据同步到其他状态管理器中。
- 让某个区块组件主动回传交互数据,例如
<input-block>。
使用方法
系统已就绪,请填写以下信息:
输入组件:
输入组件:
<template>
<div class="demo-card">
<Input v-model="text" :simulate-text="exampleText" />
<div class="render-area">
<StreamContains v-model:data="blocks" :model-value="text" mode="accurate">
<MyInputBlock />
</StreamContains>
</div>
<!-- 实时数据监控 -->
<div class="state-monitor">
<div v-for="b in blocks" :key="b.id" class="state-item">
<span class="tag">[{{ b.tagName }}]</span>
<span class="payload">已上报数据: {{ b.payload || '等待输入...' }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref, h, defineComponent } from 'vue'
import { StreamContains } from 'stream-ui'
import Input from '../../.comm/Input.vue'
// 定义一个简单的交互式拦截组件
const MyInputBlock = defineComponent({
name: 'MyInputBlock',
props: ['reportData', 'label', 'isClosed'],
setup(props) {
return () => h('div', { class: 'custom-input-box' }, [
h('span', props.label || '输入组件:'),
h('input', {
class: 's-raw-input',
placeholder: props.isClosed ? '请输入并回传' : '流式生成中...',
disabled: !props.isClosed,
onInput: (e) => props.reportData(e.target.value)
})
])
}
})
const exampleText = '系统已就绪,请填写以下信息:\n<my-input-block label="用户名"></my-input-block>\n<my-input-block label="邮箱地址" />'
const text = ref(exampleText)
const blocks = ref([])
</script>
<style scoped>
.demo-card {
padding: 1rem;
border: 1px solid var(--vp-c-border);
border-radius: 8px;
background: var(--vp-c-bg-soft);
display: flex;
flex-direction: column;
gap: 1rem;
}
.render-area {
padding: 12px;
background: var(--vp-c-bg);
border-radius: 6px;
border-left: 3px solid var(--vp-c-brand);
}
.state-monitor {
font-family: monospace;
font-size: 12px;
}
.state-item {
display: flex;
gap: 8px;
margin-bottom: 4px;
padding: 4px;
background: var(--vp-c-bg);
border-radius: 4px;
}
.tag {
color: var(--vp-c-brand-1);
font-weight: bold;
}
.custom-input-box {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0;
padding: 8px;
border: 1px dashed var(--vp-c-border);
border-radius: 6px;
}
.s-raw-input {
border: 1px solid var(--vp-c-border);
border-radius: 4px;
padding: 2px 8px;
background: var(--vp-c-bg-soft);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.s-raw-input:focus {
border-color: var(--vp-c-brand-1);
}
.s-raw-input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>数据结构解析
blocks 数组中的每一项都包含基本的 category 属性,用于区分该区块的类型:
component: 表示该区块已被识别为已注册的自定义子组件(例如你拦截的<think>标签)。fallback: 表示该区块由内置的默认标签渲染(通常是未拦截的标签或普通文本)。
StreamBlockData 接口
ts
interface StreamBlockData {
/** 区块唯一 ID */
id: string
/** 区块分类 */
category: 'component' | 'fallback'
/** 原始标签名称 */
tagName: string
/** 内部内容 */
content: string
/** 是否已闭合 */
isClosed: boolean
/** 子组件通过 reportData 回传的任意数据 */
payload?: unknown
}子组件回传数据
如果你的标签组件接收了 reportData 属性(类型为 Function),它可以把交互结果实时回传给父容器。回传的数据会反映到 v-model:data 中对应区块项的 payload 字段里。
这使得你可以实现复杂的流式交互:LLM 输出一个交互组件(如 <input />),用户在组件内操作,你的应用可以从 v-model:data 中直接读取到用户的输入状态。
代码示例
vue
<!-- 自定义输入组件 -->
<script setup>
const props = defineProps(['label', 'reportData', 'isClosed']);
const onInput = (e) => {
// 将数据回传给 StreamContains
props.reportData({ value: e.target.value });
};
</script>
<template>
<div class="input-block">
<span>{{ label }}</span>
<input @input="onInput" :disabled="!isClosed" />
</div>
</template>自闭合标签支持
Stream UI 完全支持自闭合标签(Self-closing tags),例如 <input /> 或 <img />。
- 立即闭合:解析器识别到
/>后会将该区块标记为isClosed: true。 - 无内容吞噬:自闭合标签不会尝试匹配后续的闭合标签,这确保了后续的流式文本能够被正确解析为独立的区块,而不会被误认为是该标签的内部内容。
- 灵活性:支持
<tag/>、<tag />以及带有属性的<tag attr="val" />格式。
注意事项
- 模式选择: 虽然
fast模式也支持基本渲染,但v-model:data在mode="accurate"(基于栈的解析) 下能提供更稳定和准确的结构化数据。 - 性能: 只有当内容发生实际解析变化时,
blocks才会更新,确保了极佳的性能表现。
