← 返回博客
架构2026-08-26 07:24:228 分钟 · 2,250 1

Demo 与产品之间的最后一公里:AI 助手产品化实录

AI 助手能答对文章里的技术题,却不认识博主自己的象棋项目。从这次失败出发,补齐多轮记忆、反馈闭环、失败可恢复与可见化交互——记录一个 RAG Demo 跨过"最后一公里"的四段工程。

上一篇写完 Agentic RAG 的当晚,我信心满满地在聊天窗里输入:「博主的象棋游戏用的什么算法?」

它回答:「博客中暂无相关内容」。

这个失败很有价值。助手能精准命中 ISR 报错这种埋在文章深处的问题,却对自己主人的象棋项目一问三不知——它读遍了我的博客,却不认识我的作品。排查下来,根因、修法和背后的一类问题都值得单独成文:这些功能单拎出来毫不起眼(一张日志表、两个按钮、一段历史注入),合起来却回答了一个更根本的问题——Demo 和产品之间差的那道沟到底是什么

差的不是模型能力,不是 RAG 算法,恰恰是这些没有存在感的工程。

一、知识面有边界 ≠ 无能为力:失败要可恢复

先说象棋问题的诊断。象棋项目的资料其实在知识库里——简历文本里明明写着「自研 Alpha-Beta 剪枝 + 极小化极大搜索」。但模型面对「象棋游戏」这个类专有名词查询时,按照工具描述选择了 search_articles 全文搜索——那条路查的是文章表,自然空手而归;空结果触发了拒答逻辑。

第一反应是去调检索阈值。但调阈值是治标:阈值放宽了,噪音也进来。

正确的修法是承认一个事实:项目名是有限枚举的,这类信息根本不该依赖向量召回归。于是把六个代表项目从简历文本里拆出来,做成结构化数据文件,配了两个专用工具:

list_projects()          → 列出全部项目(名称/标签/一句话简介)
get_project(name=象棋)   → 名称模糊匹配,返回技术栈与实现要点

get_project 直接读结构化 JSON,零 embedding 依赖,URL 级精确。同时给 search_articles 补了一条「指路」机制——空结果时不再返回干巴巴的空数组,而是:

{"note": "文章库未命中。若问题涉及博主的个人项目或经历,
         请改用 list_projects 或 get_project 工具查询"}

下一轮模型看到提示,自动换道调用 get_project(name=象棋),答出 Alpha-Beta 剪枝,来源指向简历页。

这一段的方法论是:Agent 区别于流水线的标志,不是会调工具,而是失败可恢复。流水线里一步落空就满盘皆输;Agent 把工具的空结果当作信号喂回去,模型自己会修正路线。你要做的只是在错误信息里告诉它「还有哪条路可以走」。

二、对话的本质是指代

第二个缺口更隐蔽:上线以来的每次提问都是独立的。用户问「他的象棋引擎用什么算法」,接着问「那篇文章的链接发我」——第二问在系统看来是一句全新的话。

解法本身很浅:请求体带上最近几轮对话,后端把它们拼进 messages 数组。真正值得写的是两个深一层的细节:

历史注入的预算控制。每条截断 600 字符、最多带 8 条——不控制的话,长对话几轮就把上下文撑爆,token 成本和延迟都会失控。

缓存语义在多轮下失效。这是我踩的最有意思的一坑。之前的相同问题缓存以「字面问题」为 key——单轮时代没问题,但多轮之后,「那第二篇呢?」这句字面相同的问题在不同上文下答案完全不同。如果继续按字面缓存,用户会收到驴唇不对马嘴的旧回答。

修复是给缓存加了语义边界:携带历史的请求绕过缓存(既不读也不写)。单轮快路径保留缓存红利,多轮路径放弃缓存换取正确性——同一句话在不同上下文答案不同,这本身就是缓存定义的反例。

三、让每次 👍 都变成改进清单

反馈按钮看起来是个 UI 活,实际做的是一条数据闭环的管道。

写入侧有个小设计:日志分两步走——回答完成时先插入骨架行(问题、工具次数、耗时)拿到自增 ID 并回传给前端,答案正文再异步补写。这样反馈接口拿到 log_id 就能立即工作,而正文落库完全不阻塞响应流。

防刷靠一行 SQL:

UPDATE ai_chat_log SET feedback = ? WHERE id = ? AND feedback = 0

feedback = 0 条件保证首次打分生效、重复提交自动忽略,不需要额外的防重表或状态机。

读取侧才是这个功能的真正价值。上线一周后,两张查询就是两份运营报告:

-- 差评样本:检索哪里失灵了
SELECT question FROM ai_chat_log WHERE feedback = -1;

-- 高频问题:访客想看什么,下一篇就写什么
SELECT question, COUNT(*) c FROM ai_chat_log GROUP BY question ORDER BY c DESC;

RAG 系统的迭代不能靠作者自嗨式的测试——真实访客的问题分布才是检索质量的试金石。差评样本暴露召回盲区,高频问题指引内容方向。没有这条管道,RAG 优化就只能靠作者想象用户在想什么。

补一笔:被放弃的问答是最贵的样本

这套管道上线后第一次自查就发现了一个盲区:日志只在回答完整生成后写入——等不及而中途关掉页面的用户,在数据里完全不存在。而这恰恰是最珍贵的负样本:中断率突增,往往意味着 LLM 服务变慢、检索质量退化,或者前端出了问题。

修复很轻:Agent 循环的每条提前退出路径都落一条标记行——客户端断开标 answer=[中断],上游异常标 [AI异常] 及原因。区分这两个标记还顺带得到两个免费的健康指标:中断率反映体验水位,异常率反映依赖稳定性。传统接口 200 毫秒返回不需要这种观测,十秒级的 Agent 循环里,它就是哨兵。

四、看得见的思考:Agent UX 三件套

最后一个缺口最反直觉:功能都对,用户还是觉得「它坏了」。

原因是 Agent 的多轮执行天然伴随等待——查列表、读文章、组织回答,一轮轮下来十几秒很正常。而这期间如果界面上只有一个静止的转圈,用户的判断就是:卡了。

解法是把系统的内部状态流式地讲出来:

[🔍 语义检索:架构演进]     ← 模型决定调什么工具,实时显示
[📖 阅读文章 #147]
[✍️ 正在组织回答…]          ← 生成前的最后一段等待
博客采用了三层架构……        ← 打字机逐字输出

三个细节:

  • 工具进度事件化:gin 在每轮工具执行前推送 status 事件,前端气泡实时替换显示——用户看到的不是黑盒等待,而是「它在翻资料」
  • 打字机节拍:伪流式切块推送必须加固定间隔(12 字 / 26ms),否则所有分片在同一毫秒到达,浏览器合并渲染成一次性蹦出——真流式的打字感来自 LLM 生成的天然节奏,伪流式得自己造
  • 确认态替换而非叠加:点赞成功后整个提问区被「✓ 已收到你的赞」顶掉,配一次弹跳动画——视线不用移动就知道成功了,比 toast 轻,比变色明确

这一段的通用原则:AI 系统的等待时间越长,「系统正在做什么」的可见性就越重要。传统接口 200ms 返回,不需要进度语义;十秒级的 Agent 循环,进度就是产品的一部分。

五、账本

四段改动合计约 400 行代码(Go + TypeScript),零新增外部依赖,月成本不变。换来的是:

维度之前之后
知识面77 篇文章文章 + 完整简历 + 结构化项目库
对话形态单问单答多轮追问、指代理解
迭代方式作者拍脑袋测差评样本 + 高频问题驱动
等待体验黑盒转圈可见化的思考过程

回头看这三篇系列——《装上一个了解我的 AI》解决从无到有,《Agentic RAG》解决能力跃迁,这一篇解决的是最容易被忽视的那段:让一个技术上成立的系统,变成一个敢放在简历上的产品

最后一公里的路没有捷径,但好消息是:它每一步都有明确的里程碑,而且每走一步,用户都会告诉你下一步在哪。


系列完整目录: ① 装上一个了解我的 AI · ② 从 RAG 到 Agentic RAG · ③ 本篇

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/152