给业务系统接入 AI 助手之后,用户不会每次都明确说“请画一张柱状图”。真正合理的体验,是 AI 能根据问题和已经获得的数据,自行判断应该返回一句话、一张表,还是一幅图。但这种“自行判断”不能靠模型随意生成 HTML,也不能让前端默认信任模型输出。它需要一份明确的展示能力协商与安全渲染契约。
很多业务后台接入 AI 后,最先实现的都是问答。
用户问:
- “今天营业额是多少?”
- “这七天销售额有什么变化?”
- “哪个门店退款最多?”
- “把本月各商品分类的销量列出来。”
第一版通常把模型的回答当作 Markdown 显示:文字可以分段,数据可以排成表格,代码可以放进代码块。这已经足以处理大量查询场景。
但很快就会出现新的需求:
当返回的是趋势、分类比较或占比数据时,AI 能不能直接生成折线图、柱状图和饼图?
技术上并不难。前端引入 ECharts、AntV 或其他图表库,模型输出一段 JSON,再把 JSON 交给图表库即可。
真正困难的不是“能不能画”,而是下面四个问题:
- 谁来判断这次回答是否应该使用图表?
- 模型怎么知道当前聊天界面真的支持某种图表?
- 模型输出的图表数据能不能被前端直接信任?
- 图表能力会不会反过来影响工具权限、审批或业务授权?
如果这四个问题没有被说清楚,所谓“AI 动态报表”很容易变成另一种不稳定的 Demo:有数据也画图,没数据也凑图;移动端显示不下;自定义前端根本不支持;更糟的是,为了渲染图表而直接执行模型生成的 HTML 和 JavaScript。
本文从一套真实业务后台的实现出发,拆解一个更完整的答案。
一、图表不是一种回答内容,而是一种客户端能力
最容易出现的设计错误,是在系统提示词里永久加上一句:
遇到数据分析问题时,尽量生成图表。
这句话看起来简单,却把三个不同的问题混到了一起。
第一,当前回答可能根本没有适合可视化的真实数值。
第二,当前客户端可能只是小程序、终端或消息机器人,根本没有图表渲染器。
第三,即使客户端能画图,它支持的图表协议和字段也未必与模型猜测的一致。
所以,模型不应该先假定“前端会画图”,再输出某个私有格式。正确方向应该是:
客户端声明自己支持什么
-> 服务端把这项可选展示能力告诉模型
-> 模型根据真实数据决定是否使用
-> 客户端只渲染自己明确允许的类型
这和浏览器的能力协商有些相似:不是服务器凭空猜测客户端能做什么,而是客户端先声明它已经安装、验证并愿意信任的展示能力。
以 BailingHub 当前的聊天协议为例,支持图表的客户端在创建聊天任务时声明:
{
"client_capabilities": {
"renderers": ["bailing-chart"]
}
}
如果客户端没有声明,模型不会收到图表展示提示,回答继续保持普通 Markdown。
这意味着:
- 使用官方聊天组件并注册了图表渲染器的 Web 后台,可以获得图表能力;
- 只实现普通 Markdown 的小程序,可以不声明;
- 自己消费 SSE 的业务前端,只能声明自己真正实现并验证过的渲染类型;
- 同一个 Agent 面对不同客户端,可以自然降级,而不需要修改业务工具或路由。
图表能力属于结果呈现层,不属于模型本身,也不属于业务权限层。
二、用户不必每次说“请画图”,但模型也不能逢数据必画
能力协商解决了“模型如何知道可以画图”,接下来才是“什么时候值得画”。
这里最重要的原则是:
图表不是为了显得智能,而是为了降低人理解真实数据的成本。
一套稳健的判断可以分成三步。
第一步:回答里是否已经有真实数值数据?
图表必须来自模型已经通过可信工具或业务接口获得的数据。
如果模型没有拿到近七日营业额,就不能为了回答“趋势怎么样”而补造七个数字;如果接口只返回今天的数据,就不能自行插值成一条折线。
所以第一道门槛不是“用户问了分析问题”,而是:
这次任务是否已经获得足够、明确、可对应到标签的真实数值?
没有真实数据时,应该说明缺少什么,而不是生成一幅看似完整的图。
第二步:图表是否明显优于文字或表格?
不同问题适合不同表达。
| 问题类型 | 更合适的输出 | 原因 |
|---|---|---|
| “今天营业额是多少?” | 一句文字或关键数字 | 只有一个结论,没有比较关系 |
“订单 SO-1001 当前状态是什么?” |
文字 | 状态事实比图表更直接 |
| “列出 5 笔待处理退款及金额” | Markdown 表格 | 用户需要逐行查看具体记录 |
| “近 7 天营业额走势如何?” | 折线图 | 重点是时间趋势 |
| “各门店本月销售额谁高谁低?” | 柱状图 | 重点是类别比较 |
| “退款原因由哪些类别构成?” | 少量分类的饼图或表格 | 重点是构成占比 |
| “为什么退款率上升?” | 结论文字 + 必要图表 | 图表呈现证据,文字解释原因与限制 |
因此,模型的可选展示提示应该明确:
- 趋势使用折线图;
- 类别比较使用柱状图;
- 少量构成占比可以使用饼图;
- 单个值、业务状态、文本解释和逐条明细优先使用文字或表格;
- 图表只有在明显更易理解时才使用。
这让用户无需每次指定图表类型,但也不会让聊天界面被无意义的可视化占满。
第三步:图表是不是只在表达结果,而没有偷换业务结论?
图表可以表达“各门店退款数量”,却不能证明某笔退款已经获批;可以显示“库存下降趋势”,却不能授权 Agent 自动补货;可以展示“异常账号数量”,却不能替代停用账号时的最终权限判断。
所以图表提示里还应该存在一条看似与 UI 无关、但非常重要的边界:
图表不能替代审批、授权、审计或业务系统校验。
展示层只是帮助人理解结果。真实业务后果仍然由原来的执行治理链路决定。
三、不要让模型生成可执行页面,只让它生成受约束的声明数据
另一种常见做法,是让模型直接返回完整 ECharts option、HTML,甚至 JavaScript。
这会带来两个问题。
1. 模型输出变成了可执行代码
一旦前端直接执行模型生成的脚本,聊天内容就不再只是数据。它可能访问页面环境、发起网络请求、读取不该暴露的上下文,或者利用渲染库与宿主页面之间的复杂接口。
即使模型没有恶意,提示注入和不可信工具结果也可能影响最终输出。
2. 图表库的私有配置污染聊天协议
ECharts、AntV、GPT-Vis 或其他库都有各自的配置格式。把某个库的完整 option 当作 Agent 协议,会让服务端、模型和业务前端都绑定到一项具体依赖。
更克制的做法是让模型只输出一个很小的声明式载荷:
```bailing-chart
{
"kind": "line",
"title": "近七日营业额",
"seriesName": "营业额",
"unit": "元",
"data": [
{ "label": "周一", "value": 12800 },
{ "label": "周二", "value": 15600 },
{ "label": "周三", "value": 14900 }
]
}
```
模型只被允许使用固定字段:
kind + title + seriesName + unit + data[label,value]
宿主前端再决定把这份数据交给 ECharts、AntV 还是其他实现。
这样可以同时保留两种独立性:
- 模型与图表库解耦:模型描述“画什么”,不描述具体库“怎么画”;
- 聊天组件与第三方依赖解耦:核心组件保持轻量,业务方按需注册渲染器。
四、为什么采用“渲染器注册”,而不是把图表库硬塞进聊天组件?
业务系统对前端依赖的选择差异很大。
有的后台已经使用 ECharts,有的使用 AntV;有的要求离线部署,有的对包体积敏感;有的只是简单问答,永远不需要图表。
如果聊天组件内置一个大型图表库,会带来:
- 组件体积和加载时间增加;
- 图表库升级节奏与聊天组件发布绑定;
- 宿主页面可能出现依赖版本冲突;
- 不使用图表的客户也必须承担成本;
- 新的富内容类型继续进入核心,最终让组件不可维护。
更合理的扩展方式,是让宿主页面显式注册受信任渲染器:
window.BailingChat.registerRenderer({
type: 'bailing-chart',
version: 1,
contentType: 'application/json',
mount({
container, payload, signal }) {
// 在这里调用宿主选定的成熟图表库。
// payload 已经过类型、字段、数据量与数值范围校验。
// signal 中止时停止异步工作并释放资源。
return () => {
// 销毁图表实例。
};
}
});
官方组件负责:
- 维护本地渲染器白名单;
- 解析完整 fenced code block;
- 对 JSON、类型、数据量和尺寸进行限制;
- 在消息替换、历史重载或会话重启时调用清理函数;
- 发生错误时安全降级为普通代码块。
图表库负责真正绘图,但不会接触聊天票据、业务凭证、可信主体身份或完整页面上下文。
五、流式输出时,为什么必须等到 done 才渲染图表?
普通文字可以边生成边显示,JSON 图表载荷却不能在只收到一半时解析。
例如模型刚输出:
{"kind":"line","data":[{"label":"周一"
此时既不是完整 JSON,也无法知道后面有多少数据。如果前端在每个 token 到达时反复创建图表,不但闪烁,还会产生大量实例和错误状态。
因此,自建流式聊天界面需要区分临时输出和权威结果:
delta -> 展示临时文字,让用户知道系统正在工作
reset -> 丢弃当前临时内容
done -> 用 done.reply 替换临时气泡,得到唯一最终回答
富内容只能在 done.reply 完整到达后解析与挂载。
这条规则也解释了为什么“我已经对接了 SSE”并不代表自动拥有官方聊天组件的所有能力。自己实现聊天前端时,还需要:
- 创建任务时声明本地真正支持的
client_capabilities.renderers; - 正确处理
delta -> reset -> done生命周期; - 把
done.reply当作唯一权威最终文本; - 只解析完整、白名单内的 fenced code block;
- 未知类型、无效 JSON、超限数据或渲染错误都降级为普通文本。
六、一个成熟的“模型自主出图”流程是什么样的?
把上面的分层收拢,一次完整过程大致如下:
1. Web 后台加载聊天组件与本地图表适配器。
2. 图表适配器向聊天组件注册 bailing-chart。
3. 聊天组件创建任务时声明支持该 renderer。
4. BailingHub 只对声明了该能力的 LLM 会话注入受约束展示提示。
5. Agent 调用真实业务工具获得数值数据。
6. 模型判断文字、表格或图表哪种表达更清楚。
7. 模型先给简短结论,必要时附带声明式图表 JSON。
8. 客户端等待 done.reply,验证载荷后交给本地图表库。
9. 载荷无效或渲染失败时,安全退回代码块。
这里有三个值得注意的“不”:
- 用户不需要每次指定“请输出柱状图”;
- 模型不能因为支持图表就猜造数据;
- 图表能力不会改变任何工具、身份、审批和业务授权。
模型只是获得一个可选表达能力,并在明确约束下决定是否使用。
七、真正的动态报表还差什么?
聊天中的单图只是第一步。企业所说的“动态报表”往往还包括:
- 多指标和多序列;
- 时间范围切换;
- 下钻到原始记录;
- 导出和分享;
- 固定仪表盘;
- 数据权限继承;
- 大数据量采样与聚合;
- 指标口径版本管理。
这些能力不能全部塞进一个聊天代码块。
更长期的结构应该仍然保持分层:
Agent 负责理解问题和提出展示意图
业务数据服务负责可信数据与指标口径
聊天协议负责传递受约束的声明
宿主应用负责交互、布局与本地渲染
业务系统继续负责数据权限和最终授权
当图表从一次回答演进成可保存、可刷新、可共享的报表时,它就应该成为业务应用里的正式对象,而不是无限扩张模型输出格式。
八、给开发者的一份检查清单
如果你正在给现有业务后台增加 AI 图表能力,可以逐项检查:
- [ ] 客户端是否显式声明自己支持的渲染类型?
- [ ] 不支持图表的客户端是否仍能收到可读 Markdown?
- [ ] 模型是否只在获得真实数值后使用图表?
- [ ] 单值、状态和明细查询是否仍优先使用文字或表格?
- [ ] 图表载荷是否为最小声明数据,而不是可执行 HTML / JavaScript?
- [ ] 渲染器是否由宿主显式注册并位于本地白名单?
- [ ] 是否限制类型、字段、数据量、尺寸和数值范围?
- [ ] 是否只在完整
done.reply后解析富内容? - [ ]
reset是否会正确清理临时气泡和已有渲染实例? - [ ] 失败时是否安全降级,而不是让整条回答消失?
- [ ] 图表能力是否完全独立于身份、工具白名单、审批与业务授权?
结语
让 AI 自动生成图表,表面上是一个前端功能,实际上考验的是系统有没有把模型能力、客户端能力、数据真实性和业务责任分开。
最好的体验不是让用户学习一套“请用折线图回答”的提示词,也不是让模型获得任意生成页面的权力。
它应该是:客户端诚实声明自己能安全展示什么,模型只在真实数据和明确价值同时存在时选择合适表达,前端只渲染受约束的声明数据,而所有业务权限与真实后果继续留在原来的治理边界内。
当这几层被分开后,文字、表格和图表不再是三个互相竞争的功能,而是同一个 Agent 根据问题结构选择的三种表达方式。
延伸阅读与实际体验
本文讨论的机制已经在 BailingHub 开源版中落地,并不只是一种界面设想。使用官方聊天组件时,客户端会显式声明自己支持的可信渲染器;模型只在拿到真实数值、且图表确实比文字或表格更有表达价值时,才输出受约束的 bailing-chart 声明。客户端不支持图表或渲染失败时,回答仍可安全降级为普通 Markdown。
这项能力只改变结果如何表达,不会扩大 Agent 能触达的业务能力,也不会绕过工具白名单、风险策略、人工审批、逐次审计和业务系统的最终授权。
- 了解 BailingHub:了解如何把 Agent 接入已有业务系统,并让查询、分析和真实业务操作经过统一治理。
- 在线体验 BailingHub:直接查看控制台、聊天入口和受治理任务的产品形态。公开体验环境请勿填写生产密钥或真实业务数据。
- 查看集成方式:查看网页组件、Direct API、Dify、n8n、MCP、OpenClaw 等接入路径。
- 可信富内容渲染器文档:了解
bailing-chart、客户端能力协商、安全降级,以及自建流式客户端需要遵守的协议约束。 - BailingHub 开源仓库:获取可自托管的开源控制面、部署文档与完整源码。
- ACC 开放契约:继续了解能力范围、风险、行动主体、审批、审计与执行约束如何被声明为可移植的治理语义。
BailingHub 是这套分层思想的可运行开源实践;ACC 是与具体产品解耦的能力声明契约。二者都不要求业务系统绑定某个模型、图表库或 Agent 平台。