游戏引擎、低代码平台和内部可视化工具通常采用“宿主对象 + 多个组件”的数据模型。用户选中场景节点后,组件编辑器读取组件元数据,展示输入框、枚举、资源选择器等控件,再把修改写回宿主。
当组件数量增加,手工填写复杂配置会逐渐成为瓶颈。例如,创建相机跟随组件时,用户需要理解目标对象、平滑系数、偏移量和更新阶段之间的关系。引入大模型后,可以允许用户输入“让相机平滑跟随 Player,并保持两米高度”这样的意图,由模型生成配置提案。
真正的工程难点并不是发出一次模型请求,而是回答以下问题:
- 模型是否可以直接修改宿主对象?
- 如何防止它生成不存在的组件、字段或对象引用?
- 一次修改涉及多个字段时,怎样保证全部成功或全部失败?
- 用户如何撤销 AI 产生的修改?
- 模型服务不可用时,原有编辑器能否继续工作?
稳妥的答案是:模型只负责提出结构化变更,编辑器宿主保留解释、验证和执行权限。
核心原理:把生成结果降级为编辑命令
组件编辑器一般包含四类对象:
HostObject:场景节点、实体或文档元素,是组件的挂载对象。Component:相机跟随、碰撞体、渲染器等可编辑能力。ComponentDescriptor:组件类型、字段类型、约束和展示信息。Command:一次可执行、可撤销的编辑操作。
AI 接入不应破坏这套关系。模型返回的不是任意代码,也不是序列化后的完整宿主,而是一组受限操作:
{
"operations": [
{
"op": "set",
"componentId": "camera-follow-1",
"path": "offset.y",
"value": 2
},
{
"op": "set",
"componentId": "camera-follow-1",
"path": "smoothTime",
"value": 0.3
}
]
}
这相当于在自然语言和编辑器命令系统之间增加一层“提案协议”。模型看不到对象实例,也不持有资源句柄;宿主根据当前选区和组件描述符构造上下文,收到结果后依次进行语法校验、语义校验和权限检查。
这里至少需要三道边界:
- 结构边界:输出必须符合 JSON Schema,禁止额外字段。
- 引用边界:
componentId、资源 ID 和宿主 ID 必须能在当前工程中解析。 - 执行边界:所有变更进入现有命令栈,不能绕过撤销、事件通知和脏标记机制。
第一步:定义最小编辑协议
下面以 TypeScript 为例。协议先只开放 set,暂不允许模型删除组件、执行脚本或访问文件系统。
type JsonScalar = string | number | boolean | null;
interface SetOperation {
op: "set";
componentId: string;
path: string;
value: JsonScalar | JsonScalar[];
}
interface EditProposal {
operations: SetOperation[];
}
interface FieldDescriptor {
path: string;
type: "string" | "number" | "boolean" | "enum" | "vector3";
writable: boolean;
minimum?: number;
maximum?: number;
enumValues?: string[];
}
interface ComponentDescriptor {
componentId: string;
componentType: string;
fields: FieldDescriptor[];
}
协议应尽量窄。比如对象引用可先用专门的 objectRef 操作表达,而不是允许任意字符串写入引用字段。新增操作类型时,也应同步补充校验器、权限规则和撤销测试。
第二步:从元数据生成上下文,而不是发送整个工程
模型只需要知道当前宿主允许编辑的字段。不要上传场景文件、脚本源码、绝对路径或与任务无关的组件数据。
function buildEditorContext(
hostId: string,
descriptors: ComponentDescriptor[]
) {
return {
hostId,
components: descriptors.map((component) => ({
componentId: component.componentId,
componentType: component.componentType,
fields: component.fields
.filter((field) => field.writable)
.map(({
path, type, minimum, maximum, enumValues }) => ({
path,
type,
minimum,
maximum,
enumValues
}))
}))
};
}
如果字段包含业务秘密或个人信息,还需要在调用前做字段级排除或脱敏。仅仅使用 HTTPS 并不能替代数据最小化、访问控制和留存策略审查。
第三步:封装可替换的模型端点
不要让编辑器面板直接依赖某一家模型服务。可以定义一个很薄的客户端,并通过环境变量配置端点、模型名和密钥。以下示例假设所选服务当前提供与示例路径和消息格式兼容的接口;实际字段、鉴权方式、结构化输出能力和模型标识必须以服务的当前文档为准。
interface ModelRequest {
instruction: string;
context: unknown;
}
export async function requestProposal(
input: ModelRequest
): Promise<unknown> {
const baseUrl = process.env.MODEL_BASE_URL;
const apiKey = process.env.MODEL_API_KEY;
const model = process.env.MODEL_NAME;
if (!baseUrl || !apiKey || !model) {
throw new Error("Missing model API configuration");
}
const response = await fetch(`${
baseUrl}/chat/completions`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${
apiKey}`
},
body: JSON.stringify({
model,
temperature: 0,
messages: [
{
role: "system",
content:
"Return one JSON edit proposal only. Use listed component IDs and writable fields. Do not return code or prose."
},
{
role: "user",
content: JSON.stringify(input)
}
]
}),
signal: AbortSignal.timeout(20_000)
});
if (!response.ok) {
throw new Error(`Model API failed with HTTP ${
response.status}`);
}
return response.json();
}
部署时可以评估官方模型接口、自建网关或包括 HaerAPI 在内的中转接口,但不能仅凭请求格式相似就假定模型、配额、日志留存或故障语义一致。
建议的环境变量如下,密钥只注入服务端进程,不写入仓库或前端构建产物:
export MODEL_BASE_URL="https://example.invalid/v1"
export MODEL_NAME="your-model-id"
export MODEL_API_KEY="set-by-secret-manager"
对于桌面编辑器,也不建议把长期密钥直接打包进客户端。更合理的方式是由受控后端签发短期凭据或代为调用,并对用户、项目和调用预算实施限制。
第四步:执行前完成结构与语义校验
JSON 能成功解析,不代表它可以执行。下面的校验器检查组件、字段、可写性、类型和数值范围:
function validateProposal(
proposal: EditProposal,
descriptors: ComponentDescriptor[]
): string[] {
const errors: string[] = [];
const components = new Map(
descriptors.map((item) => [item.componentId, item])
);
for (const [index, operation] of proposal.operations.entries()) {
const component = components.get(operation.componentId);
if (!component) {
errors.push(`operations[${
index}]: unknown component`);
continue;
}
const field = component.fields.find((item) => item.path === operation.path);
if (!field || !field.writable) {
errors.push(`operations[${
index}]: field is not writable`);
continue;
}
if (field.type === "number") {
if (typeof operation.value !== "number") {
errors.push(`operations[${
index}]: expected number`);
continue;
}
if (field.minimum !== undefined && operation.value < field.minimum) {
errors.push(`operations[${
index}]: below minimum`);
}
if (field.maximum !== undefined && operation.value > field.maximum) {
errors.push(`operations[${
index}]: above maximum`);
}
}
if (
field.type === "enum" &&
(typeof operation.value !== "string" ||
!field.enumValues?.includes(operation.value))
) {
errors.push(`operations[${
index}]: invalid enum value`);
}
}
return errors;
}
生产实现可使用成熟的 JSON Schema 校验库完成第一层验证,但对象是否存在、字段是否可写等工程语义仍需由宿主判断。还应限制操作数量、路径长度和请求体大小,避免异常输出造成资源消耗。
第五步:通过复合命令原子执行
多字段修改应作为一个撤销单元。先解析并保存旧值,全部验证通过后再执行;任何一步失败都不应留下半完成状态。
interface Command {
execute(): void;
undo(): void;
}
class CompositeCommand implements Command {
constructor(private readonly commands: Command[]) {
}
execute(): void {
const completed: Command[] = [];
try {
for (const command of this.commands) {
command.execute();
completed.push(command);
}
} catch (error) {
for (const command of completed.reverse()) command.undo();
throw error;
}
}
undo(): void {
for (const command of [...this.commands].reverse()) command.undo();
}
}
界面上应先展示差异预览,例如“smoothTime: 0.1 → 0.3”,由用户确认后再压入命令栈。对于删除对象、覆盖资源、修改脚本入口等高影响操作,建议始终要求人工确认,不因模型声称“已验证”而跳过宿主规则。
可执行落地顺序
- 盘点现有组件描述符、属性写入入口和撤销系统,确认 AI 操作能够复用同一条编辑链路。
- 只选一个低风险组件试点,并开放少量标量、枚举字段。
- 定义 JSON Schema,同时实现组件 ID、字段权限和数值范围校验。
- 将模型调用放入独立适配器,通过环境变量配置端点,设置超时和请求大小上限。
- 增加差异预览、确认按钮和单次撤销能力。
- 记录请求 ID、宿主 ID、提案摘要、校验结果和执行结果;日志中不记录密钥和未经处理的敏感字段。
- 编写固定输入测试,覆盖未知组件、越界数值、额外字段、重复操作、超时和部分执行失败。
常见问题
可以让模型直接返回组件源码吗?
可以把源码生成设计成另一条隔离流水线,但不应与属性编辑协议混用。生成的源码需要经过静态检查、依赖白名单、人工审查和受限构建环境;仅依靠提示词不能形成安全边界。
为什么设置低温度仍然要校验?
温度影响采样方式,不提供结构正确性、权限合规性或业务正确性的保证。即使服务支持结构化输出,也只能减少格式错误,不能证明引用对象存在或修改符合当前工程状态。
流式输出是否适合编辑命令?
流式输出适合展示解释文本,但不宜边接收边修改宿主。应在完整提案接收、解析和校验后一次提交。若需要实时反馈,可以流式展示“生成中”的文本区域,但执行入口必须等待完整结果。
用户发起请求后切换了选中对象怎么办?
请求中应携带宿主 ID、组件版本或编辑快照标识。响应回来时重新比对当前状态;不一致则丢弃提案或要求重新生成,不能默认应用到新的选区。
服务故障会不会阻塞编辑器?
AI 能力应是可取消的异步辅助功能。超时、限流、鉴权失败或响应不合法时,关闭生成状态并保留手工编辑路径。不要在主线程同步等待网络请求,也不要自动无限重试非幂等操作。
总结
给组件编辑器增加 AI 能力,本质上不是把聊天窗口嵌入工具,而是设计一条受控的意图转换链路。模型负责把自然语言转换为有限的结构化提案;组件描述符定义可编辑范围;宿主完成引用解析、权限检查和状态校验;命令系统提供原子执行与撤销能力。
先从低风险字段、最小协议和人工确认开始,可以让模型服务保持可替换,也让编辑器在模型不可用时继续正常工作。决定系统可靠性的不是提示词写得多复杂,而是模型输出越界时,宿主是否仍能明确地拒绝执行。