客服知识整理是我做 AI Agent 落地项目时最头疼、也最容易被低估的一环。模型选型、Runtime 搭建、工具调用这些都有成熟方案可以抄但“把团队脑子里那点经验变成 Agent 真正能用的东西”一直没有标准答案。今天想聊的这套“问答卡片”方案是我在开源 AI Agent Runtime 项目里反复调出来的路子不把知识库当文档库堆而是把知识切成一问一答的卡片让 Agent 在运行时能精准取用。这篇就把整体思路、整理流程、最小可复现的实操代码以及踩过的坑一次说清楚。适合谁看正在做智能客服、知识问答 Agent或者想让自己团队的 FAQ 真正被 AI 用起来的人。不需要多深的机器学习背景懂一点 Python、用过 API就能跟着搭起来。1. 先搞清楚Agent Runtime 和问答卡片到底是怎么配合的1.1 我理解的开源 Agent Runtime它到底解决了什么问题很多人提起 Agent第一反应是“调用大模型就行了”。但真到生产环境你会发现光有大模型远远不够——你得有一个东西负责解释用户的问题、决定调用哪个工具、记住上下文、控制生成流程这就是 Runtime 干的事。我理解的开源 Agent Runtime一句话它是 Agent 的“运行环境 执行调度器”。它本身不存业务知识只提供能力比如接收并解析用户输入、维护会话上下文、按计划调度工具调用模型、输出最终回复并记录中间过程。市面上的开源项目里LangGraph 偏图编排AutoGen 偏多智能体对话Dify 偏低代码应用搭建选哪个取决于你的场景复杂度但它们的核心职责都一样把“模型 工具 记忆”串成一个可重复执行、可观测的流程。打个比方Runtime 像餐厅的后厨管理体系传菜、排单、协调岗位都靠这个系统但每道菜怎么做还得靠菜谱。这里“菜谱”就是知识卡片。没有菜谱的后厨再好的厨师也只能对着空气发挥——同理没有结构化知识的 Agent Runtime模型再强也答不出一句可靠的话。1.2 问答卡片不是给用户看的是给 Agent 消化用的传统客服场景里“问答配对”很常见比如官网 FAQ“如何退款”——“在订单页提交申请……”。这种人工整理出来的条目人类看着很舒服但直接丢给 Agent 用往往并不理想。因为人工写的 FAQ 带大量隐含前提需要读者能“脑补”而 Agent 不会脑补。所以我要强调一个观点问答卡片是“给 Agent 看的中间表示”不是最终文案。它的目标是让 Agent 在回答前能快速判断“这张卡对应哪个问题范围”然后把答案当作参考答案结合用户具体语境重新组织语言。一张卡片的内部结构至少要包含这些字段字段作用card_id全库唯一标识question标准问法variants同义问法、口语问法category一级/二级分类用于检索过滤tags关键词标签辅助召回answer核心答案支持 Markdown 或纯文本evidence答案来源文档路径/工单编号version版本号配合知识更新signature内容指纹用于去重和变更检测review_status审核状态pending / approved / rejected看到这儿你应该明白卡片是“结构化知识单元”而 Runtime 是“执行引擎”。知识不结构化Runtime 再强也是空转。很多团队花了大量精力搭 Runtime却把知识这边随手丢一堆 PDF 让模型自己“理解”这就是典型的“重引擎、轻燃料”跑起来全是幻觉。1.3 为什么“整理成卡片”比“直接丢进向量库”更实用这是我在真实项目里反复对比后的结论也直接决定了我推荐这套方案。先声明我不是否定向量检索而是不建议“拿原始文档切片直接进向量库”就完事。第一可解释性。原始文档切片后直接进向量库检索出来是一段一段的原文运维的人根本不知道 Agent 为什么选这一段。卡片带标准问题和答案边界每一次回答都能溯源到具体卡片编号和 evidence 字段出了事故能快速复盘。第二答案质量可控。文档切片往往把一句话拆成两半模型读到不完整的上下文就容易瞎编。卡片在生成时就经过校验答案结构完整幻觉空间小很多。我见过不少项目线上跑着好好的一换文档版本模型突然开始一本正经胡说原因就是新切片把关键数字截断了。第三更新和维护成本低。客服知识是活的产品改版、活动下线都要求知识快速更新。文档库切片更新后历史记录一团乱卡片有版本号和指纹哪张过期、哪张被改动一眼就能看出来。运营同学不用懂技术也能在表格视图里改卡片内容。第四检索效率高。问答卡片天然适合“先粗筛、再精排”Runtime 先用分类和标签把候选卡片缩小到几十张再算相似度选最合适的比直接对几十万个切片做向量检索快一个量级。所以我的结论很直接如果你的目标是“让 Agent 稳定回答客服问题”那就别偷懒去走“文档全量灌进去”的捷径老老实实把知识切卡片。切完卡片Agent Runtime 才有真正可依赖的“菜谱”。2. 知识整理的完整流程从原始文档到可用卡片2.1 第一步盘点知识源先做“减法”再排序接到一个客服知识库项目我做的第一件事不是写代码而是把所有知识源拉出来盘点官网 FAQ、产品手册、售后政策、历史工单、企业微信群里运营同学的口头答复……全摆到桌面上。然后做减法。判断标准只有一个这些知识是不是“会被用户反复问到的事实性内容”是就收编不是就暂时不做。比如“常见报错及解决办法”“退款时效”“配送范围”这种必须收而“本季度销售总结”、“内部培训材料”这种则不属于客服问答场景切了也是浪费。我还特别提醒一点历史工单是金矿但也是深渊。工单里的回答散、口语化严重、还带情绪直接拿去做卡片模型学着学着就“骂人”了。我的做法是工单只用来抽取“问题点”答案一律以官方文档和运营确认过的口径为准。整理知识源的时候我还会给每个来源打个分更新频率、权威性、覆盖范围按得分排优先级。先处理那些“高频、权威、更新快”的来源低优先级的等主链路跑通再说。2.2 第二步把长文档拆成“知识单元”而不是简单按字符数切这一步是整套流程的分水岭也是最容易被做成“伪需求”的地方。很多人会把长文档按 500 字或 800 字直接切块再丢给模型。我强烈不推荐。字符切块没有语义边界经常把一个完整问题拆到两段里后面的生成和检索全受影响。举一个真实例子某产品的《退款说明》里有一句“退款将在 3-5 个工作日内原路退回”按 500 字硬切后上一段结尾是“退款将在 3-5 个工作日内”下一段开头是“原路退回节假日顺延”。模型读到任何一段都不完整生成的卡片自然是残缺的。我用的方法是“结构优先切分”先按文档层级章、节、标题、列表、表格把内容拆成块再把语义相关的相邻块合并保证每个知识单元内“话题唯一”。判断标准很简单一个单元只讲一个主题单元内有完整的上下文能被单独理解单元长度尽量在 300~800 字之间太短信息不够太长模型容易跑偏。比如一份《售后政策》文档我先按“退款条件”“退货流程”“运费承担”“争议处理”分成四个大块再分别切成若干可独立回答问题的段落。这个切分动作我用脚本辅助做 60% 的工作剩下 40% 靠人工看一眼边界毕竟机器很难理解“这个表格其实是在解释上一段那一句话”。拆分工具我推荐用 unstructure 或自己写个按标题层级递归分块的小脚本重点是把标题层级信息当成切分锚点而不是只数字符。2.3 第三步用 LLM 把知识单元改写成问答对三个关键技巧这是“整理成问答卡片”的核心环节。我试过几种方案最后稳定下来的是一条带校验的生成管线后面第 3 节会给出可运行代码。这里先说思路。对每个知识单元我让模型做三件事提取这个单元能回答的所有“用户问题”。注意是用户视角的问法不是文档小标题。比如文档里写“本产品支持蓝牙 5.3”用户问的是“连不上蓝牙怎么办”“支持蓝牙吗”你得让模型把后两种问法也生成出来。为每个问题生成一个简洁、准确、可直接使用的答案。答案要“自成一体”——即使脱离原文档用户也能看懂不能出现“如上所述”“详见下文”这类指代词。给这个单元打分类和标签方便后续检索时缩小范围。分类标签最好先定义一套固定的分类树让模型做选择题而不是填空题准确率会高很多。关键技巧是让模型“生成问题”和“生成答案”分开做而不是一次输出整个卡片。先批量生成问题清单再逐一对答案做二次生成和校验这样能显著减少“问题平平无奇、答案张冠李戴”的情况。比如“退款多久到账”和“退款多久能到银行卡”这两个问题一次生成的模型很容易只写一种问法分开做会好很多。另外一个技巧是同义问法不能靠模型一次性想全。我的做法第一轮单模型生成第二轮换一个不同 temperature 的会话专门做“补问法”任务两轮结果合并去重差不多能覆盖七八成常见问法。剩下的要靠线上真实用户问题回流不断补充 variants。因为客服场景里用户最常说的往往是“怎么还没退款”而不是“退款时效是多久”这种口语化表达靠模型生成是不够的。2.4 第四步校验、去重、入库三步缺一不可生成完不是结束反而是问题的开始。我见过很多团队生成的卡片“看起来很美、用起来翻车”就是因为少了校验环节。校验我从三个维度做准确性抽查卡片答案是否忠实于知识单元有没有模型自己加的私货。我会让模型给每条答案标注“置信来源”再人工抽检 20% 左右抽检不通过就返回重新生成。抽检比例不能太低尤其首批卡片宁可慢一点也要把尺度定好。覆盖度对照原始知识单元检查“这个单元里用户最关心的问题是否都被覆盖到了”。宁可多生成三条边缘问题也别漏掉一个高频问题。漏了高频问题的卡片上线后 Agent 会频繁转人工后台一看全是同一个盲区。一致性同一个知识点如果出现在多个知识单元生成出来的答案不能打架。我用“答案指纹 人工复核”来解决凡是同一 topic 的卡片答案相互矛盾全部打回。最典型的是新旧文档混着放一份写“运费 10 元”另一份写“运费 15 元”模型各信各的卡片就对不上。去重也很有意思。用文本相似度去做去重会误杀很多合法卡片——比如“退款多久到账”和“退款为什么还没到账”看起来像但其实是两个问题。我的经验是去重看“标准问题层”不看“同义问法层”。只要标准问题不重复就允许存在多条同义问法分属不同卡片同一张卡片内部再对同义问法去重。最后一步入库。我建议卡片落到数据库里时同时保留三个视图人类可读的表格视图运营同学用来维护、JSON 结构化接口Runtime 用来读取、以及向量索引用来语义检索。三者同步靠 card_id 关联。别想着只存一种形态后面改起来会很痛苦。卡片入库也不是一次性的事每周都要有新卡进来、旧卡被标记失效得把它当成一个“活的发布流程”来对待。3. 实操用开源组件搭一个最小可用的“知识卡片管线”我知道光讲思路不过瘾下面给一套能跑起来的最小实现。技术栈我故意选得很朴素Python FastAPI SQLite 一个兼容 OpenAI 协议的 LLM 接口全部开源组件没有重依赖。3.1 组件选型为什么这么选FastAPI给卡片生成服务提供一套 HTTP 接口也方便后续把生成能力接进 Agent Runtime。选它主要是因为生态成熟、异步支持好没必要为了炫技引入重型框架。如果有团队偏好用 Flask 也行核心不在框架。SQLite小规模知识库几千张卡片完全够用而且零部署、好备份。等卡片量真的到了十万级再平滑迁到 PostgreSQL 或 Qdrant接口层我已经用 SQLAlchemy 隔离好了。LLM 接口只要兼容 /v1/chat/completions 的模型都能用不管是本地部署的开源模型还是云上 API统一走同一个协议。这里不挑厂商关键是让管线不被某一家绑死。强调一下这套管线不是产品本身而是“知识的预处理工厂”。工厂跑完产出的 JSON 卡片才是给 Runtime 吃的东西。所以我把生成、校验、入库做成了独立的服务和 Runtime 完全解耦这样你可以单独调卡片质量不用每次动线上 Agent。3.2 数据模型一张问答卡片的真实结构我直接给建表语句你照着抄就能用CREATE TABLE qa_cards ( card_id TEXT PRIMARY KEY, question TEXT NOT NULL, variants TEXT NOT NULL, -- JSON array 字符串 category TEXT NOT NULL, tags TEXT NOT NULL, -- JSON array 字符串 answer TEXT NOT NULL, evidence TEXT, -- 来源路径 version INTEGER DEFAULT 1, signature TEXT, -- 内容指纹 created_at TEXT DEFAULT (datetime(now)), updated_at TEXT DEFAULT (datetime(now)), review_status TEXT DEFAULT pending -- pending / approved / rejected );字段看着多每个都有用card_id用 UUID不搞自增方便卡片从暂存库合并到正式库时不冲突variants存 JSON 数组比如[怎么退款, 退款流程, 退款怎么操作]review_status是我后加的字段给人工审核留了状态机否则卡片满天飞没人知道哪些能用signature存答案的哈希值用于后续判断“这条知识改没改过”。下次导入新知识单元时只要比对 signature就知道答案要不要重新审核省掉大量重复工作。我在实际项目里还会加一张card_logs表记录卡片每次被 Agent 检索和采用的情况。这张表是后期迭代的命根子没有它你就不知道哪张卡片是高频功臣、哪张卡片一年到头没被用过。3.3 核心代码卡片生成与校验先写一个最小工具函数负责把知识单元通过 LLM 转成候选卡片import json import hashlib import uuid from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keylocal-test-key) def generate_candidates(unit_text: str, category: str) - list[dict]: prompt f你是客服知识库的整理助手。 请阅读下面的知识单元生成3个用户最可能问的问题 每个问题给出标准答案并尽量补充同义问法。 要求 1. 问题必须是用户视角的口语问法不要用文档标题。 2. 答案必须能脱离原文单独读懂避免“如上所述”这类指代。 3. 如果没有把握就在答案里标注“以官方最新政策为准”。 知识单元 {unit_text} 请只输出 JSON格式如下 {{cards: [{{question: ..., variants: [...], answer: ...}}]}} resp client.chat.completions.create( modelqwen2.5-7b-instruct, messages[{role: user, content: prompt}], temperature0.3, ) content resp.choices[0].message.content # 兼容模型偶尔输出 markdown code block 的情况 content content.strip() if content.startswith(): content content.strip() if content.startswith(json): content content[4:] data json.loads(content) return data[cards]这段代码里有几个细节是踩坑换来的JSON 解析加容错。很多模型会老老实实输出纯 JSON但也有模型喜欢把结果包在json代码块里不做这段兼容生产线上会直接炸。我甚至见过模型在 JSON 后头加一句“希望对你有帮助”的所以严格的解析逻辑里还要考虑截取第一个{到最后一个}之间的内容。temperature 设低。卡片生成是知识整理任务不是创意写作0.2~0.4 比较稳太高容易让模型自由发挥、往答案里加不存在的细节。输出约束写进提示词。明确“只输出 JSON”可以省掉大量后处理但别完全信任后处理容错必须保留。接下来是校验函数重点解决“答案是否忠实于原知识单元”def validate_card(candidate: dict, unit_text: str) - dict: prompt f判断下面的答案是否忠实于知识单元。 如果答案包含知识单元中不存在的信息或与单元内容矛盾请在 verdict 填 reject。 如果答案基于单元内容但有所概括填 approve。 知识单元 {unit_text} 问题{candidate[question]} 答案{candidate[answer]} 只输出 JSON{{verdict: approve 或 reject, reason: 一句话原因}} resp client.chat.completions.create( modelqwen2.5-7b-instruct, messages[{role: user, content: prompt}], temperature0.0, ) result json.loads(resp.choices[0].message.content.strip()) candidate[verdict] result[verdict] candidate[reason] result[reason] return candidate有人会问用同一个模型既生成又校验不是“自己审自己”吗没错单模型自审只能挡住一部分明显幻觉所以我把它定位成“第一道闸门”后面必须接人工抽检。等预算充足再换成不同厂商的模型做交叉校验效果会再上一个台阶。我在一个项目里试过用两个不同系列的开源模型互相校验幻觉率能再降一半左右代价是生成成本翻倍你得根据业务风险来权衡。生成和入库的主流程def pipeline(raw_chunks: list[dict]): approved [] for chunk in raw_chunks: cards generate_candidates(chunk[text], chunk[category]) for c in cards: c validate_card(c, chunk[text]) if c[verdict] approve: c[card_id] str(uuid.uuid4()) c[evidence] chunk[source] c[signature] hashlib.sha256(c[answer].encode()).hexdigest() c[variants] json.dumps(c[variants], ensure_asciiFalse) c[tags] json.dumps([chunk[category]], ensure_asciiFalse) c[review_status] pending approved.append(c) return approved跑了这一步得到的就是一批“初审通过、待人工确认”的卡片。别急着上线先让运营同学过一遍把明显错误标出来再批量改 review_status。我一般会把审核界面做成简单的表格每行一张卡运营只需要在“通过 / 打回 / 修改”三个按钮里选几分钟能审完几百张。3.4 接入 Agent Runtime让 Agent 学会“先查卡再说话”卡片生成完最后一公里是接入 Runtime。我采用的模式很常见Runtime 里注册一个knowledge_lookup工具Agent 在回答前先调用它检索卡片。工具内部逻辑分四步先把用户问题做意图分类过滤掉不在卡片覆盖范围内的问题。比如用户问“今天天气怎么样”就直接走闲聊或转人工不碰知识库。用分类 标签做粗筛只保留相关品类下的卡片。比如用户问题落在“退款”分类就没必要把“配送”分类的几百张卡捞进来。在粗筛结果上用 embedding 算相似度取出 top 3 卡片。这里的相似度模型可以用开源的 embedding 模型本地跑完全没问题。把卡片的标准问题、同义问法和答案一起拼进 prompt让模型基于卡片内容组织回复。同时告诉模型“如果卡片不足以回答就明确说不知道不要编造”。这里有个经验不要把几百张卡片的全文都塞给模型Retrieval 一定要做两段式粗筛。否则 prompt 会迅速撑爆上下文模型还会被大量无关卡片干扰、答非所问。我踩过一次当时图省事把“退款”分类下 200 张卡片全拼进去结果模型洋洋洒洒写了一大段把“退款超时”和“退款到账时间”两个完全不同的知识点混在一起说用户直接投诉。两段式检索的效果我做过一个简单对比方案平均响应时间答案准确率人工评分直接全文进 prompt8~12s61%分类粗筛 top3 精排1~2s89%差距很明显不需要多解释。Runtime 层真正要做的就是把这个工具接入 Agent 的工具调用列表让 Agent 学会“先查卡再说话”。如果你用的是开源 Runtime本质上就是照着它的工具协议写一个函数注册进去改改系统提示词半小时能搞定。4. 常见问题与排查实录4.1 生成的问答对“答非所问”先查这三个地方症状卡片问题问的是“退货运费谁承担”答案却在讲“退货申请流程”。排查思路先看知识单元本身是不是就讲了两件事如果是切分阶段就没切干净回炉重切。这是最常见的原因切分没做好后面全白搭。再看模型生成时的 prompt 是否给了足够约束我在 prompt 里加了“答案必须直接回答问题不要发散到相邻话题”有帮助。最后看是不是同义问法污染了检索用户在模糊检索时匹配到了别人的卡片这时要调整 variants 的去重策略不同问题之间不能共用同一问法。比如“退款流程”和“退款条件”都可能被用户说成“怎么退款”但这不是一回事两个卡片的 variants 不能混。4.2 卡片太多Agent 检索不过来几千张卡片之后纯 SQL 的 LIKE 查询明显变慢top3 检索质量也下降。我的处理方案粗筛阶段加一层“分类倒排”先在几十个分类里命中一两个再查分类下的卡片。相当于先翻目录再翻正文而不是从第一页一直翻到最后。给category和tags建索引SQLite 里这一招立竿见影。别小看这个卡片量上来之后一张复合索引能省好几倍的查询时间。如果卡片到了十万级把向量检索换成独立的向量库别让 SQLite 硬扛。我推荐用 Qdrant开源、支持过滤条件、部署简单和阿里的开源镜像也没关系就是个普通的开源数据库。4.3 答案过期了没人发现三层机制兜底这是所有客服知识项目里最隐蔽的坑。产品活动 8 月结束卡片还写着“全场 8 折”用户来问Agent 一本正经地答错这个责任就要算到知识维护头上了。我的解决思路分三层顶层运营侧定时任务每周扫一遍updated_at超过 30 天的卡片标成“待确认”。这是最笨但最有效的方法逼着运营同学定期回访知识。中层每个卡片设effective_date和expire_dateRuntime 检索时自动过滤过期卡片。活动类知识最需要这个设置一个到期日到期自动失效不用人肉去删。底层线上反馈闭环用户点了“这个答案没用”的会话自动抽取出对应卡片进人工复核队列。用户已经用脚投票了你还不改就是自欺欺人。三层都做了才敢说“知识不过期”。我后来还在卡片详情页里加了一个“最近 7 天被引用次数”的统计运营一打开后台就知道该维护哪张卡不用猜。4.4 卡片覆盖不了冷门问题学会“不知道”比硬答更重要再全的知识库也会有用户问出你没想到的问题。这时候别硬答Agent 得学会“不知道”。我在 Runtime 里设了兜底规则检索 top 卡片的相似度低于阈值比如 0.65就回复“这个问题我暂时无法确认建议转人工”同时把这句话连同用户问题记录到日志。这比让模型瞎编一个答案安全得多——客服场景里错了是要赔钱的。我还试过让 Agent 在“不确定”时主动反问用户“您是想问 A 还是 B”能救回不少冷门问题。这个对话策略写进系统提示词里成本几乎为零效果不错。尤其当用户问题带歧义时反问比猜测稳定得多。日志里那些被反问后用户选择“是”的会话就是下一次新增卡片的最好素材等于让用户帮你标注知识边界。4.5 多张卡片答案互相打架先检查来源和版本这个坑我遇到过两次。一次是同一张卡片从旧文档和新文档各生成了一版答案不一样另一次是两个不同部门各提供了一份口径不一致的说明模型干活的时候左右横跳。解决方案是给每张卡片增强evidence的权重凡是答案以官方最新文档为准的打上source_rank high其他来源的答案在入库时就要经过更严格的复核。同时生成卡片时我会按来源版本号分组同一个 topic 永远只允许一个活跃版本其他版本降级为“参考”不进 Runtime 的检索范围。这样从源头上掐掉打架的可能。5. 最后补一句我的真实体会这套“问答卡片”方案我在三个不同场景的客服项目里跑过最大的感受不是技术多复杂而是**“知识整理”这件事决定了 Agent 的上限**。Runtime、模型、工具链大家都能买到开源方案唯独“把经验变成结构化卡片”这步必须靠团队自己对业务的理解去完成。如果你要落地我建议从一个小分类开始挑 50 个高频问题手工和模型配合先做出 200 张左右覆盖良好的卡片跑通“卡片生成 → 人工审核 → Runtime 检索 → 线上反馈”的闭环再逐步扩展到全量知识。先小后大远比一上来就想做全库更稳。全库模式的难点不在于生成而在于维护知识一变几百张卡都要跟着动没有流程很容易崩。最后再分享一个习惯生成完一批卡片后我会强制自己在一天内回来看一遍真实对话日志看哪些问题是卡片没接住的。每次看都能发现一两个“我以为覆盖了其实没有”的盲区。知识库是养出来的不是一次建成的。你把它当成一个持续运营的产品来做Agent 的表现就会一直往上走你要是当一次性项目做完就撒手那翻车只是时间问题。