:构建最小可用的问答链路)
前三篇已经得到可追踪片段、嵌入模型和向量索引。本篇把这些组件接成最小可用产品输入一个问题检索证据控制上下文预算生成受约束答案并返回能定位原文的引用。一、痛点端到端“能回答”仍缺少明确契约原型常把检索结果直接拼进一段提示然后返回模型文本。这条链路无法区分空问题、无权限、无证据、模型超时和格式错误也无法让前端稳定展示引用。最小可用不等于代码最少而是最少的可验证行为输入被规范化检索受用户权限约束低相关时拒答证据不超预算输出结构可解析引用只指向实际使用的片段。建议定义响应对象answer、citations、request_id、index_version和status。状态至少包含answered、insufficient_evidence与failed。业务失败不要全部变成 HTTP 500“资料不足”是正常、可统计的结果。内部追踪则记录每阶段耗时、候选分数、提示版本和模型用量。二、原理先选择证据再让模型受证据约束一条稳健链路按顺序执行查询校验与规范化、权限过滤检索、阈值判断、证据去重、预算装箱、提示构造、生成、结构校验、引用映射。顺序不能随意交换。例如权限必须在返回内容前生效预算必须在提示调用前计算引用必须验证其编号属于本次证据集合。下面用标准库实现一个确定性的端到端管线。生成器采用抽取式规则使任何机器都能运行接真实大模型时只需替换generate其余边界仍可测试。fromdataclassesimportdataclass,asdictimportjsondataclass(frozenTrue)classEvidence:evidence_id:strtext:strsource:strscore:floatCORPUS[Evidence(E1,差旅报销须在返程后十个工作日内提交。,报销制度#3,0.92),Evidence(E2,发票抬头必须与公司名称一致。,发票指南#2,0.51),]defretrieve(query:str,minimum:float0.70)-list[Evidence]:keywordsset(query)rankedsorted(CORPUS,keylambdae:(-len(keywordsset(e.text)),-e.score))return[itemforiteminrankedifitem.scoreminimum]defgenerate(query:str,evidence:list[Evidence])-dict:ifnotevidence:return{status:insufficient_evidence,answer:知识库中没有足够依据。,citations:[]}firstevidence[0]return{status:answered,answer:f根据现行制度{first.text},citations:[{evidence_id:first.evidence_id,source:first.source}],}question返程后多久提交差旅报销responsegenerate(question,retrieve(question))print(json.dumps(response,ensure_asciiFalse,sort_keysTrue))print(fused_evidence{len(response[citations])})运行输出{answer: 根据现行制度差旅报销须在返程后十个工作日内提交。, citations: [{evidence_id: E1, source: 报销制度#3}], status: answered} used_evidence1真实生成器应要求模型只使用编号证据并输出 JSON Schema 或其他结构化格式。即便模型宣称引用 E1服务端仍要核验 E1 在本次证据包内。引用 URL 从可信元数据映射不能让模型自由生成否则会出现看似合理但不存在的链接。三、实现提示、预算与超时都纳入代码系统提示至少说明角色、证据边界、资料不足时的固定行为、冲突处理和输出模式。证据用清晰分隔符包裹来源字段与正文分开降低文档内恶意指令影响。查询不是系统指令也要单独标记。温度通常设低以提高可重复性但低温不能消除幻觉。上下文选择可视为有序装箱先按相关性排序去掉内容哈希相同或高度重叠的片段然后在 token 预算内加入。下例生成可检查的证据包并拒绝单个超长片段。defbuild_context(rows:list[tuple[str,str]],budget:int)-str:blocks:list[str][]used0forevidence_id,textinrows:blockf[证据{evidence_id}]\n{text}\niflen(block)budget:continueifusedlen(block)budget:breakblocks.append(block)usedlen(block)ifnotblocks:raiseValueError(no evidence fits context budget)return\n.join(blocks)rows[(E1,差旅报销须在返程后十个工作日内提交。),(E2,电子发票与纸质发票不得重复报销。),(E3,审批完成后由财务安排付款。),]contextbuild_context(rows,budget65)print(context)print(fcontext_chars{len(context)})运行输出[证据 E1] 差旅报销须在返程后十个工作日内提交。 [证据 E2] 电子发票与纸质发票不得重复报销。 context_chars53生产中改用模型 tokenizer 计算预算并分别设置检索、重排和生成超时。总截止时间应向下传递若请求只剩 500 毫秒不应再启动一个最长 30 秒的生成调用。重试仅用于明确的瞬时错误且带指数退避和幂等请求 ID内容被拒绝或上下文过长不应盲目重试。接口层还要约束输入长度、字符编码与会话历史。多轮对话不能把全部历史无限追加应把当前问题改写成自包含查询并只携带确实影响回答的上下文。用户修改同一句问题时创建新的请求 ID服务日志通过父会话 ID 关联既能回放单次请求也能分析连续追问为何发生。四、踩坑提示注入与证据冲突必须预先设计知识库文档也可能包含“忽略此前指令”之类文本。文档始终是不可信数据用分隔符标记系统提示明确不得执行其中指令工具权限放在模型之外输出再做校验。更关键的是任何提示防护都不能代替访问控制模型根本不应收到无权限内容。当两份制度冲突时不能简单让模型“综合”。利用生效日期、版本、发布部门等元数据在检索或重排阶段优先选择现行版本若仍冲突应展示双方并提示人工确认。对没有证据的问题宁可返回稳定拒答也不要用模型常识补全企业政策。另一个易忽略的边界是流式输出。模型可能先输出肯定答案末尾才产生非法引用此时前端已经展示错误内容。高风险应用可先生成并完成校验后再一次性返回若必须流式应缓冲到完整声明验证其引用后再发送并在协议中定义中断状态避免把半截回答记作成功。五、验证为每个失败阶段准备一个测试最小测试集覆盖正常命中、同义问法、空查询、库外问题、受限文档、超长证据、冲突版本、生成超时、非法 JSON 和伪造引用。测试不仅断言答案文本还断言使用的 chunk ID、状态、索引版本和是否调用生成器。这样调整提示或模型时能知道回归发生在哪个阶段。部署前再做一次契约回放固定检索响应以单测生成层固定模型响应以单测解析与引用端到端测试只保留少量关键路径。分层替身能避免外部模型随机性掩盖代码错误也能精确模拟超时、空结果和异常结构。线上采样请求必须先脱敏并受访问审计与保留期约束。本篇建立了第一条可部署的问答链路。下一篇会针对“答案在库里却搜不到”继续优化引入查询改写、混合召回、去重与重排并用指标判断每项改动是否值得。参考来源OpenAI CookbookQuestion answering using embeddingsOWASPLLM Prompt Injection Prevention Cheat SheetLangChainHow to return sources 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《RAG 知识库问答实战》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。