让 LLM 学会嵌入图表:Lakemind 内联图表引用机制的实现解析

📅 2026/7/13 2:46:13
让 LLM 学会嵌入图表:Lakemind 内联图表引用机制的实现解析
让 LLM 学会嵌入图表Lakemind 内联图表引用机制的实现解析做 AI Agent 产品时有一个常见痛点LLM 生成了图表但在最终文字总结中引用图表时要么贴一段裸 Markdown 图片语法![alt](url)让用户看到一堆乱码要么只能用文字干巴巴地描述如上图所示。最近在 Lakemind 中实现了一套内联图表引用机制让 LLM 能像写 Markdown 链接一样在文字总结的任意位置嵌入一张可交互的图表。这篇文章拆解实现思路。问题背景Lakemind 的 Agent 在分析数据时会调用render_chart工具生成 ECharts 图表。工具执行完毕后Rig 框架会把工具返回值注入到对话上下文中LLM 据此知道图表已生成。但在最终文字总结中引用图表时出现了两个问题LLM 倾向用![销售额趋势](url)这种 Markdown 图片语法但 Lakemind 的图表不是静态图片而是 ECharts 实例——这个语法无法渲染只会显示成裸文本即使告诉 LLM “不要用图片语法”它也无法在文字流中精确地表达在这里插入刚才那张图表解决方案{{chart:id}}标记协议核心思路是设计一套轻量标记协议让 LLM 在文本中嵌入标记前端解析标记并原地渲染图表。Rust 端render_chart 工具工具执行成功后不再只返回一句简单的摘要而是返回一个包含标记的字符串letmarker{{chart:.to_string()call_id}};Ok(format!({}。在结论中引用此图请将以下标记原样粘贴到结论对应位置{},user_summary,marker))这里用String拼接而不是format!宏原因是format!会把{{解析为字面量{导致标记被转义。call_id是本次工具调用的唯一标识同时也是前端 ChartSegment 的id保证了标记和图表的一一对应。PREAMBLE 提示词引导在系统提示词中新增两条规则正面引导在结论中引用图表时将{{chart:...}}标记原样粘贴到对应位置该处会原地渲染为交互式图表负面禁止禁止使用![alt](url)等 Markdown 图片语法引用图表图表不以图片形态存在前端解析文本分段与图表匹配新增chartRef.ts负责文本拆分和图表查找MessageText.tsx负责渲染。拆分逻辑constCHART_REF_RE/\{\{\s*chart:\s*([^}]?)\s*\}\}/g;exportfunctionsplitTextByChartRefs(text:string):TextChunk[]{// 用正则把文本切成交替的 text / chartRef 片段// 流式输出时未闭合的 {{chart: 会留在尾部 text 片段中// 等 }} 到达后自动愈合为标记}图表查找三级降级exportfunctionfindChartSegment(segments:Segment[],ref:string){// 1. 精确 id 匹配——主路径// 2. 标题模糊匹配——LLM 用记忆中的标题引用时// 3. 数字序号 {{chart:1}} → 第一张图——最简引用}渲染组件Show when{hasRefs()} fallback{MarkdownRenderer content{props.text} /} For each{chunks()} {(c) ( Switch Match when{c.kind text} MarkdownRenderer content{c.content} / /Match Match when{c.kind chartRef c.chart} ChartSegment seg{chart()} / /Match Match when{c.kind chartRef} span classchart-ref-missing 图表引用未找到/span /Match /Switch )} /For /Show当没有任何标记时直接走MarkdownRenderer零回归。当标记无法匹配到图表时显示一个 muted badge 而不是泄露原始{{...}}花括号。流式输出的边界情况Agent 的回复是流式的——文字逐字到达图表 segment 可能比文字标记晚到。MessageText用createMemo依赖props.segments当新的 chart segment 到达时自动重新解析标记会从未找到状态自愈为正常图表。设计要点回顾决策选择理由标记格式{{chart:id}}双花括号避免与 Markdown 语法冲突id 来源工具调用的call_id天然唯一且就是 segment id拼接方式String::to_string()避免format!的花括号转义陷阱查找降级id → 标题模糊 → 序号兼容 LLM 的不精确引用无标记时直接走 MarkdownRenderer零回归保证这套机制的核心洞察是不要试图让 LLM 生成完美的富文本而是给它一套简单的标记协议让前端做重活。LLM 只需要复制粘贴一个标记剩下的事情——文本拆分、图表匹配、原地渲染、异常降级——全部由前端代码保证。相关链接Commithttps://github.com/tsingliuwin/lakemind/commit/43055e254ab5b81bf4d3d7003027576d83e21b5dGitHub 仓库https://github.com/tsingliuwin/lakemind许可证AGPL-3.0