跳到主要内容

注册模型:把组件和工具变成 LLM 能调用的东西

30 秒导读: Tambo 让 AI「渲染你的 React 组件」。但模型并不认识 React——它只会调用带 JSON Schema 的函数(工具)。本章讲前端作者侧的「注册」这一层:你把一个组件连同它的 propsSchema(用 Zod 写)交给 Tambo,Tambo 怎么把它一步步变成模型能挑、能调用的东西。不讲后端怎么决策(见 02-decision-loop)、也不讲流式事件(见 03-agui-streaming)。


1. 这一层要解决什么(零基础也能懂)

先说清楚这层在整条链路里的位置。

一个大语言模型(LLM)本身只会产文本发起工具调用(tool call)——即「我要调用名为 X 的函数,参数是这个 JSON」。它完全不知道 <Graph> 是什么、React 是什么。

所以要让模型「用你的 UI」,得先把 UI翻译成模型的母语:一个有名字、有参数 schema 的工具。这就是「注册层」干的事。

一句话定义: 注册层 = 把「React 组件 + 它的 props 规格」登记进一张表,并把这张表翻译成模型能读懂的工具清单,随每次对话请求发给后端。

给谁用: 写 Tambo 应用的前端作者。你唯一要做的,是像下面这样描述你的组件——剩下的翻译全自动。

// 来自 README「How It Works」的教学例
const components: TamboComponent[] = [
{
name: "Graph",
description: "Displays data as charts using Recharts library",
component: Graph,
propsSchema: z.object({
data: z.array(z.object({ name: z.string(), value: z.number() })),
type: z.enum(["line", "bar", "pie"]),
}),
},
];

直觉类比:propsSchema 想成函数签名,把 description 想成给模型看的函数文档字符串。模型读文档、按签名填参数、"调用"这个组件——Tambo 收到调用后就把对应组件渲染出来。「注册」就是把组件登记成一个可被调用的函数。

本节不碰底层代码。你只要记住:注册层是「React 世界 → 工具世界」的翻译官。


2. 顶层全景(它大概怎么转)

这一层是一条流水线:从「作者写的对象」流到「发给后端的请求体」。先看整体流向,再逐段拆。

怎么读这张图:从上到下是一次注册 + 一次发消息的数据流;每个方框右边小字是真实符号名,方便你去源码里 grep

作者侧(React 组件树)
│ 写下 TamboComponent { name, description, component, propsSchema: z.object(...) }

① 登记进注册表 ───────────────────────────── registerComponent()
│ 校验 + 把 propsSchema 转成 JSON Schema validateAndPrepareComponent()
│ 存成 RegisteredComponent{ props: JSONSchema } → componentList

② 发消息时,把整张表翻成 API 格式 ──────────── toAvailableComponents()
│ 组件 → AvailableComponent{ name, description, propsSchema }
│ 工具 → Tool{ name, description, inputSchema } toAvailableTools()

③ 随 run 请求发给后端 ─────────────────────── client.threads.runs.run({ availableComponents, tools })

④ 后端把每个组件包成一个工具(第 2 章)────── show_component_<Name>
组件与工具在这里被统一成「模型能调用的函数」

各部件一句话职责:

部件干什么在哪个文件
TamboComponent作者描述一个组件的对象(名字/描述/组件/propsSchema)packages/client/src/model/component-metadata.ts:276
TamboRegistryProvider持有 componentList / toolRegistry 两张注册表react-sdk/src/providers/tambo-registry-provider.tsx:161
validateAndPrepareComponent校验组件、把 propsSchema 转成 JSON Schema 存起来react-sdk/src/util/registry-validators.ts:178
schemaToJsonSchema通用「Standard Schema → JSON Schema」转换器packages/client/src/schema/schema.ts:64
toAvailableComponents / toAvailableTools把注册表翻成 API 请求需要的数组packages/client/src/utils/registry-conversion.ts:58 / :132
UI_TOOLNAME_PREFIX组件工具名前缀 show_component_packages/core/src/ui-tools.ts:1

注意两张表:组件componentList,工具toolRegistry(见 tambo-registry-provider.tsx:173-174)。它们最终并肩发给后端——这正是本章标题「把组件工具变成 LLM 能调用的东西」的含义:在模型眼里,二者都只是工具。


3. 核心机制(逐个拆)

3.1 组件带 propsSchema 注册

要解决的小问题: 模型填参数前,得先知道「这个组件收哪些 props、每个是什么类型」。作者用 Zod 写一次 propsSchema,既给 TypeScript 做类型、又给模型做参数规格——一份来源,两处受益。

propsSchema 的类型是 SupportedSchema:任何符合 Standard Schema 规范的校验器(Zod 3.24+/Zod 4、Valibot、ArkType…)一段原始 JSON Schema 都行。

// packages/client/src/model/component-metadata.ts:310 —— propsSchema 字段
propsSchema?: SupportedSchema;

注册发生在 registerComponent:它先跑校验+转换,再把结果塞进 componentList

// react-sdk/src/providers/tambo-registry-provider.tsx:278 —— registerComponent 内
const { props } = validateAndPrepareComponent(options);
// props 已经是 JSON Schema,存进注册表:
// componentList[name] = { component, name, description, props, contextTools: [] }

validateAndPrepareComponent 是守门人,它强制三条规矩(registry-validators.ts:178):

规矩违反时
必须提供 propsSchema(或已废弃的 propsDefinition)抛错:必须有其一
不能两个都给抛错:只能用一个
propsSchema 里不能含 record 类型assertNoRecordSchema 抛错

转换本身在 getSerializedProps:是 Standard Schema 就调 schemaToJsonSchema,是 JSON Schema 就原样收下,都不是就抛「Invalid props schema」(registry-validators.ts:139-170)。这体现了仓库的 fail-fast 风格——存进注册表的 props 永远已是 JSON Schema,后面的转换层不必再猜类型。

关键细节: associatedTools——注册组件时可以顺带带上一组工具(TamboComponent.associatedTools,component-metadata.ts:320)。registerComponent 会把它们一并注册进 toolRegistry 并建立关联(tambo-registry-provider.tsx:296-302)。这让「这个组件配套要用的取数工具」跟组件一起登记。

3.2 propsSchema 怎么转成 JSON Schema

要解决的小问题: 作者写的是 Zod 对象,但 API 和模型只认 JSON Schema。需要一个同步、能同时吃 Zod 3 和 Zod 4 的转换器。

核心是 schemaToJsonSchema:已经是 JSON Schema 就直接返回,否则走 Standard Schema 的同步转换。

// packages/client/src/schema/schema.ts:64 —— schemaToJsonSchema
export function schemaToJsonSchema(schema: SupportedSchema): JSONSchema7 {
if (!isStandardSchema(schema)) {
return schema; // 已经是 JSON Schema,原样返回
}
return toJsonSchema.sync(schema) as JSONSchema7;
}

真正的 Zod 兼容技巧藏在模块加载时注册的 vendor 处理器里:Zod 4 的 schema 带 _zod 标记,走 Zod 4 原生转换;否则回落到 zod-to-json-schema(Zod 3)

// packages/client/src/schema/schema.ts:28 —— loadVendor("zod", ...)
if (schema && typeof schema === "object" && "_zod" in schema) {
return zod4ToJSONSchema(...) as JSONSchema7; // Zod 4 原生
}
return zodToJsonSchema(...) as JSONSchema7; // Zod 3 回落

为什么要同步转换? 因为注册和发消息都在渲染路径上,不想为一次 schema 转换引入 await@standard-community/standard-json.sync 让它一步到位。

3.3 从注册表转成 API 需要的 AvailableComponent / Tool

要解决的小问题: 注册表里存的是「带 React 组件引用的富对象」,但请求体只能带可序列化的元数据。得剥掉组件本体,只留 name / description / schema。

组件转换极简——因为 props 在注册时已经是 JSON Schema 了,这里只做搬运和缺失校验:

// packages/client/src/utils/registry-conversion.ts:31 —— toAvailableComponent
if (!component.props) {
throw new Error(`Component "${component.name}" missing props - required for API`);
}
return {
name: component.name,
description: component.description,
propsSchema: component.props, // 直接用注册时转好的 JSON Schema
};

toAvailableComponents(:58)是它的批量版:遍历整张表,单个组件转换失败只 console.warn 跳过,不整批崩——这是「一个坏组件不该拖垮整次对话」的取舍。

工具侧对称:toAvailableTool(:93)对每个工具的 inputSchemaschemaToJsonSchema,产出 { name, description, inputSchema };toAvailableTools(:132)是批量版。

这三个转换在哪被调用? 发消息时,一次性把两张表都翻好塞进 run 请求:

// packages/client/src/utils/send-message.ts:258 —— 发消息前
const availableComponents = toAvailableComponents(componentList);
const availableTools = toAvailableTools(toolRegistry);
// 随后 client.threads.runs.run(threadId, { availableComponents, tools: availableTools, ... })

到这里,前端作者侧的活就干完了:组件和工具都变成了随请求发出的 JSON 元数据。后面交给后端。

3.4 命名约定:组件最终变成 show_component_<Name> 工具

要解决的小问题: 组件发到后端时还只是 AvailableComponent,不是工具。得给它一个工具名,让模型能「调用」它。

约定就一条:组件名 Graph → 工具名 show_component_Graph。前缀是一个共享常量:

// packages/core/src/ui-tools.ts:1
export const UI_TOOLNAME_PREFIX = "show_component_" as const;

拼接动作发生在后端(属于 02-decision-loop 的地盘,这里只点出命名规则本身):后端把每个 AvailableComponent 包成一个 OpenAI 工具,名字就是前缀 + 组件名。

// packages/backend/src/services/tool/tool-service.ts:108 —— convertComponentsToUITools
name: `${toolNamePrefix}${component.name}`, // toolNamePrefix 默认 = UI_TOOLNAME_PREFIX
description: `Show the ${component.name} UI component the user. Here is a description of the component: ${component.description}`,

反向识别用 isUiToolName:凭前缀就能判断「这个工具调用其实是要渲染一个组件」,而不是普通工具。

// packages/core/src/ui-tools.ts:3 —— isUiToolName
return typeof toolName === "string" && toolName.startsWith(UI_TOOLNAME_PREFIX);

作者要记住的落地约束: 因为组件名会被直接拼进工具名,组件 name 必须是合法工具名(assertValidName 会校验)。给组件起名时,当它是在给一个函数起名。

3.5 两类组件:generative(渲染一次) vs interactable(持久可更新)

要解决的小问题: 有的组件是「答一次就完事」(图表、摘要);有的要「持续存在、被反复改」(购物车、任务板)。两者在注册层的待遇不同。

generative(生成式)interactable(可交互)
语义回一条消息、渲染一次持久实例,可被后续消息反复更新
怎么注册放进 TamboProvidercomponents 列表withTamboInteractable HOC 包裹
模型能对它做什么show_component_<Name> 渲染它额外获得更新 props / 更新 state 的工具
例子图表、数据可视化便签、购物车、电子表格

generative 就是 3.1–3.4 讲的路径。interactable 的不同,在于它会为每个实例自动注册一对「更新工具」——这样模型不仅能"造"它,还能"改"它。

包裹用 withTamboInteractable(README 里简称 withInteractable,真实导出名是前者):

// 教学例(改自 README「Interactable Components」)
const InteractableNote = withTamboInteractable(Note, {
componentName: "Note",
description: "A note supporting title, content, and color modifications",
propsSchema: z.object({
title: z.string(),
content: z.string(),
color: z.enum(["white", "yellow", "blue", "green"]).optional(),
}),
});

HOC 挂载时调 addInteractableComponent(with-tambo-interactable.tsx:139),后者给这个实例分配唯一 id、并注册两个工具:

// react-sdk/src/providers/tambo-interactable-provider.tsx:437 —— addInteractableComponent 内
registerInteractableComponentPropsUpdateTool(newComponent); // update_component_props_<id>
registerInteractableComponentStateUpdateTool(newComponent); // update_component_state_<id>

这两个工具的名字前缀分别是 update_component_props_update_component_state_(:301:362),inputSchemamakeJsonSchemaPartialpropsSchema/stateSchema 变成可只传部分字段的偏 schema——因为模型往往只想改其中一两项。它们默认带 tamboStreamableHint: true,意味着更新可以随流式增量到达(细节见 03-agui-streaming05-rendering-interactables)。

一句话对照:generative → 一个 show_component_<Name> 工具;interactable → 额外一对 update_component_props_/state_<id> 工具。 前缀不同,模型据此知道该"造"还是该"改"。

3.6 普通 TamboTool(informational 工具)与组件的对称

要解决的小问题: 有些能力不渲染任何 UI,只是「取一段数据/做一次计算」(查天气、算经纬度)。这就是普通工具。

TamboTool 的形状和组件高度对称——都是「名字 + 描述 + 一份 schema」,差别只在:组件带一个要渲染component,工具带一个要执行tool 函数。

// packages/client/src/model/component-metadata.ts:111 —— TamboTool(节选)
name: string; // 工具名
description: string; // 给模型看的用途说明
tool: (params) => MaybeAsync<...>; // 真正执行的函数
inputSchema: SupportedSchema<Params>; // 参数规格(会转成 JSON Schema)
outputSchema: SupportedSchema<Returns>; // 返回结构(告知模型,当前不做运行时校验)

对称关系一张表看清:

组件(component)工具(tool)
注册表componentListtoolRegistry
参数 schema 字段propsSchemainputSchema
「本体」是什么要渲染的 React 组件要执行的函数
转成 APIAvailableComponentTool
模型调用后Tambo 渲染组件Tambo 执行函数、把结果回灌

正因为对称,后端能把二者统一成同一份工具清单丢给模型(tool-service.ts:215getToolsFromSourcescomponentTools + clientToolsSchema + mcpToolsSchema 拼在一起)。在模型眼里没有「组件」和「工具」之分,只有一串可调用的函数——这就是整个注册模型的收敛点。


4. 巧妙之处(可借鉴)

  • 「一次转换,存起来复用」。 propsSchema注册时就被转成 JSON Schema 存进 componentList(registry-validators.ts:178tambo-registry-provider.tsx:280),所以发消息路径上的 toAvailableComponent 几乎零成本、也不必再判断 schema 类型(registry-conversion.ts:31)。热路径干净。
  • Standard Schema 抽象解耦了具体校验库。 注册层只依赖 SupportedSchema 接口,不绑死 Zod;Zod 3/Zod 4 的差异被隔离在 loadVendor 一个回调里(schema.ts:28)。作者换库不影响这层。
  • 命名前缀当协议用。 show_component_ / update_component_props_ / update_component_state_ 三个前缀,让「工具名」自带语义:后端/客户端凭 startsWith 就能路由(isUiToolName,ui-tools.ts:3),不需要额外的类型字段。
  • 批量转换容错跳过而非整批失败。 toAvailableComponents / toAvailableTools 对坏条目只 warn 跳过(registry-conversion.ts:73:145),单个组件写错不会让整次对话发不出去。

5. 边界与局限(诚实)

  • propsSchema 不能是 record 类型。 assertNoRecordSchema 会拦下(registry-validators.ts:201)。开放键值对的 props 不被支持——因为要转成模型可读的固定 schema。
  • outputSchema 只用于告知模型,不做运行时校验。 类型上写了,注册层不校验工具真实返回(见 component-metadata.ts:176-183 的注释)。
  • interactable 的 id 长度有上限。 更新工具名 = 前缀 + 组件 id,受 maxNameLength(默认 60)约束,id 过长会抛错(tambo-interactable-provider.tsx:303)。
  • 本章只到「发出请求」为止。 组件如何被模型挑中、props 如何流式回来、interactable 如何被渲染并接更新——分别属于 02 / 03 / 05。注册层只负责把「能调用什么」讲清楚给后端。

6. 代码地图(导航索引)

主题文件路径符号名
组件/工具的作者侧类型packages/client/src/model/component-metadata.tsTamboComponent · TamboTool · SupportedSchema
React 侧组件类型覆写react-sdk/src/model/component-metadata.tsTamboComponent(ComponentType)
两张注册表 + 注册入口react-sdk/src/providers/tambo-registry-provider.tsxTamboRegistryProvider · registerComponent · registerTool
注册校验 + props 转 JSON Schemareact-sdk/src/util/registry-validators.tsvalidateAndPrepareComponent · getSerializedProps · validateTool
通用 schema 转换器packages/client/src/schema/schema.tsschemaToJsonSchema · loadVendor("zod")
注册表 → API 格式packages/client/src/utils/registry-conversion.tstoAvailableComponent · toAvailableComponents · toAvailableTool · toAvailableTools
转换的调用点(发消息)packages/client/src/utils/send-message.tssendThreadMessage(见 :258)
UI 工具命名约定packages/core/src/ui-tools.tsUI_TOOLNAME_PREFIX · isUiToolName
组件→工具名拼接(后端)packages/backend/src/services/tool/tool-service.tsconvertComponentsToUITools · getToolsFromSources
interactable 包裹 HOCreact-sdk/src/hoc/with-tambo-interactable.tsxwithTamboInteractable
interactable 的更新工具react-sdk/src/providers/tambo-interactable-provider.tsxaddInteractableComponent · registerInteractableComponentPropsUpdateTool · registerInteractableComponentStateUpdateTool

下一章: 后端拿到这份 availableComponents + tools 后如何决策、挑组件、流式吐 props → 02-decision-loop