说明本文讨论的是模型直接产出可渲染结构卡片、表单、图表、待办、操作按钮之后前端渲染层与模型之间的接口怎么定属于 AI 前沿动态话题不涉及具体模型版本与价格。AI 领域版本迭代极快凡涉及版本号、价格、可用性请以你阅读时的官方页面为准。文中代码为结构示意请按自己的技术栈调整后再上生产。一、输出形态变了验收标准也跟着变了两种消费者容错能力差了不止一个量级文本输出的消费者是人。人对残缺、歧义、口语化有很强的补全能力一句话没说完能猜字段少列一个能靠上下文推。结构输出的消费者是程序程序不猜——组件名差一个字符就是取不到值类型不对就是渲染不出来。同一份产出人看是大致清楚了程序看是这块渲染不了。验收标准从读得懂换成了渲染得出文本形态下的端到端质量靠人评价相关性、通顺度、信息量。结构形态下多出一层前置判断——这份产出能不能被渲染器吃下去。这一层是布尔的只有成或不成而且必须排在人评价之前。把它当渲染细节往后放会出现内容质量评估全部通过、用户看到空白的情况。失败表现从答得不好变成页面坏了文本答错用户看到的是答得不对心里有数可以追问。结构渲染失败用户看到的是空白、是错位、是一块红色报错条。用户拿不到任何可继续的信息连该不该再问一次都判断不了。更麻烦的是代价不对称文本错了只影响这一次回答结构错了会连带同一条产物里其他块一起看不见。维度文本输出结构化渲染输出消费者人程序渲染器容错能力高残缺也能靠上下文猜低字段或类型不符即失败验收标准读得懂、可采纳先渲染得出再谈内容好坏典型失败表现答得不对、答得空空白、错位、报错条影响范围一次回答可能拖垮同一产物里的其他块兜底责任生成侧渲染侧生成侧不可控是否带副作用不带可能带操作按钮要改的是接口层不是提示词层这一变化主要落在接口层。把提示词打磨到能输出一段结构并不难难的是这份结构由谁定义、按什么规则渲染、渲染层不认识的时候怎么办。另外划一条线怎么让模型稳定产出合法结构是生成侧的事本文不展开只讨论结构到手之后渲染层怎么接。二、渲染契约放在哪一层三种放法只有一种能长期用渲染器定义契约前端维护一份组件注册表模型只能从已注册的组件里选。模型侧定义契约模型自由产出结构前端想办法适配。中间翻译层模型产出中性描述由翻译层映射到具体组件。长期能用的只有第一种。理由很朴素渲染层知道自己的能力边界——哪些组件已经实现、哪些平台还不支持、哪些字段后端还没有数据源模型不知道。让不知道边界的一方定契约结果就是源源不断地产出渲染不了的东西。第二种在初期勉强能用等结构开始嵌套组合适配代码会先变成一坨、再变成故障源第三种多了一层没有权限判断能力的映射最终仍要回到第一种。契约追求的是可渲染不是表达力契约的目标不是能表达多少种结构而是渲染侧一定能渲染出来。所以组件数量要克制先用十来个覆盖高频场景比一次铺开几十个、最后小一半渲染不了要好。未注册的组件不是暂时没实现而是永久不渲染。把这条定死生成侧就会主动收敛。未知字段与未知类型向前兼容的三条最小规则未知字段忽略不报错。生成侧先上线、渲染侧后上线时窗口期里必然出现渲染侧不认识的字段。未知组件类型只降级当前块。不要把整条产物判为失败其余认识的部分照常渲染。已知组件但字段不符能补默认值就补补不了才降级该块。强转类型只会制造更隐蔽的错误。三条规则合起来是一个取向单块失败不拖垮整篇。局部降级永远优于整页白屏。契约的长相与写权限⚠️ 代码待验证# 渲染侧的组件注册表可渲染集合由渲染侧定义模型只能在这个集合里选components:-type:text_blockrequired:[text]optional:[tone]-type:cardrequired:[title]optional:[subtitle,body,image_url,actions]-type:key_value_listrequired:[items]# items: [{label, value}]-type:chartrequired:[chart_type,series]optional:[x_label,y_label]-type:todo_listrequired:[items]# items: [{text, done}]-type:action_barrequired:[actions]# actions 的风险等级见第三章比组件高一档# 未知输入的处置策略集中在渲染侧声明不散落在各组件实现里unknown_component:degrade_to_text# 只降级当前块unknown_field:ignore# 静默忽略不报错missing_required:degrade_to_texttype_mismatch:degrade_to_textmax_nesting_depth:3# 防止渲染成本失控注册表的写权限必须在渲染侧。生成侧只能提需求“希望能出一张对比表”不能直接往里加类型。否则渲染侧永远追不上生成侧产出的种类白名单也就名存实亡。输入情况处置理由未知字段忽略继续渲染生成侧先行时不至于炸未知组件类型仅该块降级为文本或占位单块失败不该拖垮整篇已知组件、必填缺失先补默认值补不了仅该块降级缺字段是局部问题字段类型不符按缺失处理强转会制造更隐蔽的错误嵌套超过声明深度截断到声明深度防止渲染成本失控组件数量超单条上限超出部分折叠防止长产物拖慢首屏三、白名单与动作边界模型产出按不可信输入对待结构从模型出来不是你能控制的数据源。它可能被用户粘贴的内容影响也可能被检索到的网页影响。所以渲染层的第一条纪律是把模型产出的结构当成不可信输入和工具返回的结果放到同一档。组件白名单的具体含义是注册表之外的一律不渲染。这里有个容易被忽略的反面要求——渲染器不能提供通用渲染能力。比如结构里带一个字段写着一段网页标记渲染器照着渲染出来这等于把页面的渲染权交给了不可信输入跨站脚本这一类问题会直接落到你头上。动作比组件危险一档组件只影响显示用户看一眼就过去了。动作用户一点会产生真实副作用提交工单、发送消息、扣款、删除记录。把动作和组件塞进同一套白名单是常见错误展示组件的代价是排版难看动作的代价是不可撤销的业务变更两者不该用同一份规则管。动作的三道闸动作白名单只有声明过的动作类型才渲染成可点按钮其余一律渲染成不可点的说明文本。注意是渲染成不可点不是隐藏——隐藏会让用户以为模型没说这件事。参数校验动作带参数时参数要按动作自己的契约校验枚举值在不在范围内、数值有没有越界、被操作对象是否属于当前用户可见范围。模型给的参数不做任何信任。二次确认加鉴权点击后的副作用必须在服务端按当前身份重新鉴权而不是因为结构里写着该用户可以执行就放行。确认弹窗要展示动作的实际影响对象哪个订单、哪条记录而不是一句是否继续。层影响面失败后果需要的闸纯展示组件仅当前页面显示排版错乱组件白名单 字段校验链接跳转跳出当前页引导到钓鱼页域名白名单表单提交写入数据脏数据、重复提交动作白名单 参数校验 鉴权不可逆动作外部可见、无法撤销业务事故上述全部 二次确认 明确影响对象权限不能由模型自证一个反复出现的错误做法让模型在结构里带一个字段声明允许执行前端照着放行。权限判断的依据只能是服务端持有的身份与业务规则不能来自产物里的字段——那条路径等于让不可信输入给自己发许可证。⚠️ 代码待验证ALLOWED_ACTIONS{open_detail,submit_form,add_to_list}# 渲染侧维护IRREVERSIBLE{submit_form}defgate_action(action:dict,session)-dict:# 1) 动作类型必须在白名单里否则连按钮都不渲染ifaction.get(type)notinALLOWED_ACTIONS:return{render:disabled_text,reason:action_not_whitelisted}# 2) 参数按动作自己的契约校验模型给的参数不做任何信任ok,detailvalidate_params(action[type],action.get(params,{}))ifnotok:return{render:disabled_text,reason:params_invalid,detail:detail}# 3) 鉴权只看服务端身份不看结构里的任何允许字段ifnotauthorize(session.user_id,action[type],action[params]):return{render:disabled_text,reason:not_authorized}# 4) 不可逆动作必须二次确认且要展示实际影响对象ifaction[type]inIRREVERSIBLE:return{render:button_with_confirm,confirm:{target:describe_target(action[params])}}return{render:button}四道判断的顺序是刻意的先白名单再看参数最后才谈确认。顺序反了会出现给一个不该存在的动作弹了确认框这种更糟的形态。四、降级路径降级是常规路径不是异常处理把降级写进异常分支是常见误解。渲染层不认识某个结构在两侧版本不同步时会持续发生它是常态就得和主路径一样被设计、测试、观测而不是在 catch 块里写一句兜底。三级降级一级原样渲染组件和字段都被认识用户看到完整交互形态。二级简化渲染组件认识但部分字段用不了。典型场景是图表后端数据源还没接好退化成键值列表或图片地址不可用只保留文字。用户拿到的是能力降一档、信息没丢的形态。三级纯文本整块不认识或解析失败退回结构里自带的可读文本。这也是为什么产物应该带一个fallback_text字段——它是渲染契约的一部分不是可选项。有一条硬约束降级由渲染侧决定不要走把结构再交给模型转成文本的回环。回环会引入一轮额外调用把延迟和不确定性都放大结果仍不可控。保底留一条回到原始数据的路三级降级时把原始结构折叠保留比如一个查看原始数据入口。两个作用用户不至于面对空白排障时能直接看到模型到底产出了什么。⚠️ 代码待验证defrender_block(block:dict,registry:dict)-dict:ctypeblock.get(type)# 未知类型只降级当前块绝不抛异常影响兄弟块specregistry.get(ctype)ifspecisNone:return{level:3,mode:text,payload:block.get(summary)or,raw:block}missing[fforfinspec[required]iffnotinblock]mismatch[fforfinblockiffinspec.get(types,{})andnotisinstance(block[f],spec[types][f])]ifmissingormismatch:# 能补默认值的先补补不了才退三级filled,still_missingapply_defaults(block,spec,missingmismatch)ifstill_missing:return{level:3,mode:text,payload:block.get(summary)or,raw:block}return{level:2,mode:simplified,payload:filled,raw:block}return{level:1,mode:full,payload:block}层级触发条件用户看到代价一级 原样渲染组件与字段都被认识完整交互形态无二级 简化渲染组件认识部分字段不可用降一档的只读形态交互能力下降信息保留三级 纯文本整块不认识或解析失败一段可读文字无交互但内容不丢保底 原始数据折叠任意降级路径可展开的原始结构排障用不建议默认展开两个不要做不要渲染失败就静默丢弃——用户看到内容凭空消失比看到报错更困惑。不要渲染失败弹错误码——错误码是给开发看的用户要的是内容本身把它给用户等于把排障工作转嫁出去。五、版本与兼容窗口三类变更危险程度差得很远加字段向后兼容。渲染侧不认识就按第二章的规则忽略掉。风险最低可以随时做。改语义同一个字段名含义变了比如时长从秒变成毫秒。这是最危险的一类两侧都不报错内容照常渲染只是显示成一个错的值。用户看到时长 90说不出它指秒还是分钟。下线组件渲染侧不再支持某个组件类型。不走废弃流程的话历史产物和生成侧新产物会在同一时间踩空。变更类型兼容性两侧是否报错推荐做法新增可选字段兼容否渲染侧先上生成侧后上新增必填字段破坏性是缺失即降级先降级为可选再逐步收紧修改字段语义破坏性且静默否最危险换成新名如带单位后缀旧名保留一个窗口期下线组件破坏性是生成侧先停产出渲染侧再摘支持调整嵌套深度上限破坏性是超限被截断提高上限安全降低上限要走废弃流程上线顺序的原则生成侧和渲染侧不可能在同一秒上线所以顺序本身是一份设计。经验做法是这样新增字段或组件渲染侧先上先会渲染生成侧后上再开始产出。窗口期里生成侧不产出、渲染侧已就绪用户看不到任何降级。修改语义不原地改含义换字段名把单位写进名字里旧名保留一个窗口期两侧同时认新旧两套。下线组件生成侧先停产出渲染侧再摘支持中间留一个观察窗口确认没有存量产物还在用。压缩成一句让渲染侧总是先知道生成侧总是后使用。反过来做窗口期里用户看到的就全是降级形态。契约版本落在哪两种做法随产物带版本号或渲染侧按内容探测。前者显式排障时一眼能看出这条产物按哪版契约产出后者省事但兼容判断散落在各组件实现里越往后越难查。倾向把契约版本放在产物的信封层和块序列放在一起而不是每个块各带一份。⚠️ 代码待验证{render_contract_version:2026.10,fallback_text:本期巡检完成率 97.2%可展开查看明细。,blocks:[{type:text_block,text:本期巡检结果如下。},{type:key_value_list,items:[{label:完成率,value:97.2%}]},{type:action_bar,actions:[{type:open_detail,params:{id:r-1024}}]}]}fallback_text和blocks平级是刻意的渲染层完全失败时至少还有一句人话可显示不至于白屏。信封层还应带上产物标识方便把渲染失败和那次生成对上。完整版资料清单本文用到的渲染契约清单、白名单分级示例与降级路径对照表都整理在里面了扫码即可获取六、文本与结构混排时的渲染时序混排才是常态真实产物很少是纯结构常见形态是一段说明、一张卡片、一段结论所以渲染层处理的单位是块序列而非单块——要维护的是顺序和位置不只是单块能不能画出来。两类块到达节奏不一样文本块内容一到就能显示长度事先不可知。结构块必须拿到完整结构才能渲染但它的高度由组件决定是事先可知的。两类块的到达节奏天然不同。按到达顺序直接往页面尾部追加页面高度会边走边跳用户正在读的段落会被后到的结构块顶走。先占位后替换处理办法很朴素结构块一旦确定会来就先按组件声明的固定高度占位骨架形态或固定高度容器内容齐了再填进去。文本块照常顺序追加。占位高度不必精确但必须在内容到达前就定下来这是整套时序成立的前提。布局只有一次提交关键纪律布局的提交点只有一个——整批块到齐或者等待超时之后才做一次性最终排布。中途只往预留位里填内容不改已提交块的位置和尺寸。这样生成过程中页面是稳定的不会出现读到一半段落跳走。多提交一次布局体验上的损失比多等几百毫秒大得多。⚠️ 代码待验证时间轴 文本块A到达 结构块B占位 结构块B就绪 文本块C到达 批次提交 页面动作 追加 A 预留 B 的固定高度 填入 B不改高度 追加 C 一次性排布 页面高度 单调增加 一次性增加 不变 单调增加 不变 B 高度确定时机 — 占位时提前 不参与 不参与 — 反面做法B 到齐时才发现它比占位高把已渲染的 A 和 C 顶走 —— 用户正在读的内容跳走。失败块的收场占位块最终失败时要收成一条可读文字不留空洞也不闪一下消失。空洞会让用户以为内容还在路上一直等闪消失会让用户以为看花了眼。本文不涉及的部分结构从字节流到完整对象的增量解析、连接中断后的重连与续传这些已有专文讨论本文不重复。这里只处理结构已经成形之后怎么摆到页面上。七、观测与落地顺序先看四个数解析失败率整条产物解析不出块序列的比例。这个数一上来就得盯它归零之前谈别的指标没意义。未知命中率拆成两个数看。未知组件类型的占比高说明生成侧在契约之外产出未知字段的占比高多数是生成侧先行的正常现象看趋势就行。降级率落到二级、三级的比例必须按组件类型拆开看。只看总量会掩盖问题——总量平稳时某个组件的降级率可能正一路走高。动作成功率按钮渲染出来之后点击到成功执行的比例同时看被鉴权或参数校验拦下的比例。拦下的比例过高说明生成侧在编造动作参数。两个容易漏的数占位超时率结构块占位之后迟迟没就绪的比例。它反映的是生成侧的速度不是渲染侧的缺陷但用户感受到的是同一件事。布局提交次数每轮应该只有一次。大于一次就说明有块在中途改了尺寸第六章那条纪律被绕过了。指标看什么异常时指向解析失败率产物无法解析为块序列的比例生成侧产出格式漂移未知组件命中率渲染侧不认识的类型占比契约未对齐生成侧越界未知字段占比被忽略的字段比例生成侧先行或契约有漏项降级率按组件拆开的二级、三级占比契约覆盖不足或字段语义漂移占位超时率占位块迟迟未就绪的比例生成侧速度非渲染问题布局提交次数每轮实际重新排布的次数大于一次说明中途改了布局动作点击成功率点击到执行成功的比例参数校验或鉴权配置过严动作拦截率被鉴权或参数校验拦下的比例过高说明动作参数是编造的落地顺序先做只读组件文本块、键值列表、卡片。没有动作风险面最小。契约、白名单、降级路径、观测口径都先在这几个组件上跑顺。再做只读但结构复杂的组件图表、时间线这一类。它们的降级路径更复杂字段背后往往还依赖后端数据源二级降级的形态要单独设计。最后做带动作的组件动作白名单、参数校验、二次确认、服务端鉴权全部就位之后再开。顺序反了等于把一个能触发真实副作用的入口交给一份不可信输入去驱动。契约版本化与废弃流程等组件数量上到十来个、生成侧开始主动提新组件需求时再强上。过早引入会拖慢前面几步那时的契约也还没稳定到值得版本化的程度。完整版资料清单本文用到的观测字段定义、落地顺序清单与动作风险分级表都整理在里面了扫码即可获取附表 A关键取舍一览本文涉及的所有工程判断集中在这里方便按需回看。工程取舍原因依据章节渲染侧定义契约生成侧只在集合内选择只有渲染侧知道自己的组件与数据源边界第二章未知字段忽略、未知类型只降级当前块单块失败不该拖垮整篇第二章契约追可渲染不追表达力表达力越界只会变成源源不断的降级第二章模型产出的结构按不可信输入对待它可能被用户输入或检索内容影响第三章动作与组件分开定白名单两者的不确定性代价不同档第三章权限判断只看服务端身份产物里的允许字段不能给自己发许可证第三章降级写成常规路径并纳入测试版本不同步时降级会持续发生第四章降级不走模型回环回环翻倍延迟且结果仍不可控第四章改语义必须换字段名原地改含义两侧都不报错最危险第五章渲染侧先知道生成侧后使用窗口期内用户看不到降级形态第五章契约版本放在信封层兼容判断集中排障时一眼可查第五章结构块提前占位、布局只提交一次避免生成过程中页面高度来回跳第六章降级率按组件拆开看总量平稳会掩盖单组件的恶化第七章先只读组件最后才上动作动作是唯一能造成不可逆后果的一层第七章附表 B术语速查表术语含义富交互产物模型直接产出的可渲染结构如卡片、表单、图表、待办渲染契约渲染侧声明可渲染集合与未知输入处置方式的约定组件注册表渲染侧维护的可渲染组件清单含必填字段与类型声明组件白名单注册表之外一律不渲染的准入规则未知组件降级只把不认识的那一块退成文本其余照常渲染动作白名单只有声明过的动作类型才渲染为可点按钮二次确认不可逆动作点击后展示影响对象并要求再次确认三级降级原样渲染、简化渲染、纯文本三个能力档位原始数据折叠降级时保留可展开的原始结构用于排障兼容窗口两侧版本不同步期间双方都要能工作的过渡期语义漂移字段名未变但含义改变两侧都不报错却显示错值占位块为尚未就绪的结构块预先按声明高度留出的容器布局提交对整批块做最终排布的那一次动作应每轮仅一次解析失败率整条产物无法解析为块序列的比例降级率落到二级或三级渲染的块占比需按组件拆开观测写在最后这篇用到的资料写这篇文章时我把渲染契约的几种定法、组件白名单分级和降级路径都对了一遍顺手整理成几份配套的东西大模型学习路线图从 LLM 基础到 Agent 开发各阶段该学什么、用什么资料大模型全套教程按主题分好的视频与文档清单大模型实战好书24 本附每本适合的阶段资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「大模型」优先通过。拿到之后建议先看学习路线图那一份先定位自己在哪个阶段再决定学什么比一上来就啃框架效率高得多。