大模型多轮对话:语义理解与上下文推理的工程实践

发布时间:2026/10/10 1:49:25
大模型多轮对话:语义理解与上下文推理的工程实践
简介这份docx文档围绕ChatGPT技术对话中的语义理解与上下文推理展开面向NLP学习者、对话系统开发者和AI技术研究者系统讲解核心方法与实践思路。包内有1个docx文件仅37KB内容精炼适合通读与批注。资料已有68人学习属于被关注的原创分享。文档从GPT模型的Transformer编码原理出发解析如何提取输入句子的语义特征进而介绍通过引入前文进行上下文推理以理解用户意图再说明注意力机制如何按相关程度分配权重避免回复自相矛盾最后讨论强化学习在优化对话质量中的作用并梳理多义词处理、长对话流畅性等现存挑战。读者可借此快速掌握ChatGPT式对话在语义建模、上下文推理及生成优化上的关键要点亦可作为课题研究、技术写作或面试复习的参考资料。1. 从“听懂”到“记得住”ChatGPT技术对话里语义理解到底卡在哪做过对话机器人的工程师大概都遇到过同一个场景用户上一句说“我要查昨天那单物流”下一句问“它到哪了”系统要么把“它”当成一个没见过的词要么把“昨天那单”理解成今天的新需求。前者是语义理解没到位后者是上下文推理没接住。ChatGPT技术对话看似是“一句进一句出”但真正决定对话质量的恰恰是这两层能力怎么配合。很多团队直接把所有对话逻辑塞进一条大模型提示词里结果线上效果时好时坏——这不是模型不行而是没人把语义理解和上下文推理当成两个独立模块去设计。本文会沿着“单句怎么听懂 → 多轮怎么记住 → 相关记忆怎么被召回 → 线上有哪些坑”这条线把一套可落地的方案完整拆开。适合正在做客服机器人、文档问答系统或者想把大模型接入自己业务对话流程的开发者。2. 语义理解从文本清洗到意图识别先让模型听懂“这一句”2.1 先分清楚语义理解不等于大模型对话管道里各层的职责各管一段我见过不少项目把“语义理解”直接等同于“调用一次大模型接口”。用户发一句话把这句话拼进提示词让模型返回一个JSON这确实简单但一旦遇到线上流量、成本控制和结果稳定性问题这种方式很快就会露馅。更可靠的做法是把语义理解拆成一条明确的管道文本规范化、意图识别、槽位填充、语义向量表示四件事各管一段。管道里每一层都有独立职责。文本规范化负责把“3Q”、“收到啦~”这类噪声转成干净文本意图识别回答“用户想干什么”槽位填充回答“这个意图需要哪些关键参数”语义向量表示则是把句子转成数值为召回和相似度计算打底。大模型在其中扮演的角色是“理解引擎”而不是“对话外包”——它负责做判断和生成但输入什么、输出怎么校验仍然由管道控制。这样拆分的好处有三个。第一每一层都可以单独测试和回滚哪一层出问题改哪一层不用整条链路推倒重来第二可以混用规则和模型确定性高的场景用正则开放性理解交给模型降低整体成本第三日志和排错变得直观你能明确知道意图错了是分类器的问题还是文本清洗把关键词弄丢了。2.2 用“意图识别 槽位填充”搭最小语义理解链路用一个具体例子来说。假设你在做一个售后客服机器人用户的输入是“我上周买的充电宝坏了想换一个”。这条输入里意图是“售后换货”槽位需要提取“商品类型充电宝”“时间上周”“问题坏了”。下面这个最小链路用组合方式完成这件事先用规则锁定确定性高的部分再用模型兜底模糊表达。import re import jieba # 意图规则表关键词 - 意图 INTENT_RULES { 换货: [换一个, 想换, 重新发, 换新], 退货: [退货, 退钱, 退款, 不想要了], 物流咨询: [物流, 到哪, 快递, 发货没, 配送], } def detect_intent(text: str) - tuple[str, float]: 先走规则返回 (意图, 置信度)。未命中返回 (unknown, 0.0) for intent, keywords in INTENT_RULES.items(): for kw in keywords: if kw in text: # 简单置信度命中关键词长度占句子比例 confidence min(0.95, 0.5 len(kw) / len(text)) return intent, confidence return unknown, 0.0 def extract_slots(text: str) - dict: 槽位提取时间、商品类型、问题描述 slots {} # 时间槽位匹配“上周/昨天/前天/X天前” time_match re.search(r(上周|昨天|前天|(\d)天前), text) if time_match: slots[time] time_match.group(1) # 商品类型用词性词典商品名词表可以后续替换成商品库 product_dict [充电宝, 数据线, 耳机, 手机壳] for word in jieba.lcut(text): if word in product_dict: slots[product] word # 问题描述意图关键词后的剩余文本片段简单截断处理 for kw in INTENT_RULES[换货] INTENT_RULES[退货]: idx text.find(kw) if idx ! -1: slots[issue] text[:idx] break return slots text 我上周买的充电宝坏了想换一个 intent, conf detect_intent(text) slots extract_slots(text) print(intent, conf, slots) # 输出示例换货 0.95 {time: 上周, product: 充电宝, issue: 我上周买的充电宝坏了}这段代码的逻辑分三步走。detect_intent先做意图判定命中关键词直接返回意图和置信度置信度公式是0.5 关键词长度/句子长度意思是关键词占句子比重越大判定越有把握但封顶0.95给模型兜底留空间。extract_slots用正则抽时间用分词加词典匹配抽商品再按“意图关键词前文”截取问题描述。参数上最需要调的是两处。第一INTENT_RULES里的关键词粒度太短会误伤比如“换”字到处出现太长会漏召回我一般控制在2到4个字。第二置信度阈值建议设0.6——低于0.6的走大模型兜底高于0.6直接走规则这样能把规则误判率压到一个可接受范围。2.3 语义向量的取舍什么时候用embedding什么时候用规则规则能覆盖的场景终究有限用户换一种说法就失效了这时候要靠语义向量做相似度召回。但向量方案有一个尴尬问题阈值不好定。同一句话在不同领域里语义相似度的分布差异很大。我见过某团队在商品客服场景把阈值定在0.78结果一上线就大量误召回换到文档问答场景同样0.78又变得太严什么都召不回来。所以我的做法是“规则优先、向量兜底”而且向量后面必接二次校验。下面这段代码演示了混合判断的写法import numpy as np # text_embedding 是预置的文本向量化函数不用纠结具体实现 def text_embedding(text: str) - np.ndarray: # 实际项目中这里接一个开源embedding模型输出768维向量 return np.random.randn(768) def cosine_sim(a: np.ndarray, b: np.ndarray) - float: return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) 1e-8)) def mixed_intent_match(user_text: str, candidate_intents: list[dict]) - dict: # 第一轮规则直出 rule_intent, conf detect_intent(user_text) if rule_intent ! unknown and conf 0.6: return {intent: rule_intent, source: rule, confidence: conf} # 第二轮向量召回候选意图 user_vec text_embedding(user_text) best None best_sim 0.0 for item in candidate_intents: # item {intent: 换货, examples: [想换一个新的, 东西坏了要换个]} example_vecs [text_embedding(e) for e in item[examples]] sim max(cosine_sim(user_vec, ev) for ev in example_vecs) if sim best_sim: best_sim sim best item[intent] # 阈值0.72命中才返回低于阈值返回unknown交给LLM if best and best_sim 0.72: return {intent: best, source: vector, confidence: best_sim} return {intent: unknown, source: fallback, confidence: 0.0}这段代码里有两个关键设计。一是候选意图需要预先准备“例句池”每个意图至少5到10句典型说法例句质量直接决定召回效果例句写得越接近用户真实口吻向量相似度越可靠。二是阈值0.72不是拍脑袋定的我一般先在测试集上跑一遍画一个“相似度分数-正确率”曲线取正确率开始明显下滑的拐点作为阈值。需要特别提醒的是source字段一定要留。线上出问题时通过这个字段能快速区分这个误判是规则造成的还是向量召回造成的没有这个字段排查时会浪费很多时间。另一个容易忽略的点是向量方案对“否定表达”天然不敏感“我不要换货”和“我要换货”语义相似度可能高达0.9所以涉及否定词的场景必须在向量前加一层否定检测。3. 上下文推理让模型记住前文并知道哪一句在影响哪一句3.1 对话状态跟踪把“记忆”显式化成结构语义理解解决的是“这一句在说什么”上下文推理解决的问题是“结合之前说过的话这一句到底在说什么”。很多团队用最粗暴的方式做上下文——把所有历史消息拼接起来喂给模型。这在轮次少时没问题轮次一多就完蛋模型分不清哪句是用户当前诉求哪句是已经被满足的旧信息。对话状态跟踪Dialog State Tracking, DST就是把记忆显式化成结构。我一般维护一个DialogState对象里面存三样东西当前意图、所有已确认槽位、最近若干轮的原始消息。每一轮新输入进来时先走语义理解管道拿到“增量意图和槽位”再去更新全局状态而不是直接覆盖。class DialogState: 显式的对话状态跟踪器 def __init__(self, max_rounds: int 10): self.slots: dict {} # 全局槽位表: {product: 充电宝, time: 上周} self.intent_history: list [] # 每一轮的意图记录 self.rounds: list [] # 原始消息轮次留作召回用 self.max_rounds max_rounds def update(self, user_text: str, intent: str, new_slots: dict) - dict: self.rounds.append({user: user_text, intent: intent}) # 只保留最近N轮防止状态无限膨胀 if len(self.rounds) self.max_rounds: self.rounds self.rounds[-self.max_rounds:] # 槽位合并策略新值覆盖旧值但空值不上写 for k, v in new_slots.items(): if v: self.slots[k] v # 意图变化记录 self.intent_history.append(intent) return self.slots def get_context_prompt(self) - str: # 把结构化的状态转成提示词片段 slot_desc , .join(f{k}{v} for k, v in self.slots.items()) return f已知信息: {slot_desc} | 最近意图: {self.intent_history[-3:]}这段代码的核心是“增量更新”和“滑动保留”。update方法接收上一轮语义理解管道的输出把新槽位合并进全局槽位表。这里有个细节容易踩坑如果模型提取出来的槽位值是空的比如这次没提到商品那么if v会阻止旧值被清掉。这非常重要因为用户的后续追问往往带着省略比如“它坏了怎么办”里的“它”指向前文的充电宝如果每次更新都清空槽位省略表达就会全部失效。max_rounds的值我建议设在8到12之间。太短会丢失早期关键信息太长会让状态对象变得臃肿而且大部分对话里用户会在5轮内敲定主要诉求后面的轮次更多是确认和补充。这个参数可以在上线后看“平均对话轮次”动态调。3.2 滑动窗口与记忆衰减上下文不是越长越好很多刚接触大模型对话系统的工程师会陷入一个直觉误区历史消息越多模型理解越准确。实际不是这样。把20轮前的闲聊全部喂给模型不仅消耗token还会引入干扰——尤其是在用户话题发生过切换的对话里老话题的消息会把新话题的推理带偏。我见过一个真实案例用户先问“手机碎屏怎么办”中间聊了一堆别的最后问“这个服务收费多少”系统基于前文的“碎屏”回答了维修价而用户其实是在问上一个话题里提到的某个会员服务的收费。对话上下文管理的常见做法是“滑动窗口 摘要压缩”。滑动窗口保证模型永远只看到最近N轮原文超过窗口的部分用一条摘要句子概括这条摘要随对话推进不断更新。下面是具体实现class ContextWindow: 滑动窗口 旧记忆摘要压缩 def __init__(self, max_tokens: int 2000, max_rounds: int 8): self.max_tokens max_tokens # prompt部分的总token预算 self.max_rounds max_rounds # 保留原文的最大轮数 self.rounds: list[dict] [] # 最近轮次原文 self.summary: str # 旧轮次的压缩摘要 def add_round(self, user_text: str, assistant_text: str, estimate_fn) - None: estimate_fn: 估算文本token数的函数可用 len(text)*1.3 粗略估 self.rounds.append({ user: user_text, assistant: assistant_text, tokens: estimate_fn(user_text) estimate_fn(assistant_text) }) # 情况A轮数超限最老的一轮退化为摘要 if len(self.rounds) self.max_rounds: old_round self.rounds.pop(0) self._merge_into_summary(old_round) # 情况Btoken超预算从最老的开始折叠 total sum(r[tokens] for r in self.rounds) estimate_fn(self.summary) while total self.max_tokens and len(self.rounds) 1: old_round self.rounds.pop(0) self._merge_into_summary(old_round) total sum(r[tokens] for r in self.rounds) estimate_fn(self.summary) def _merge_into_summary(self, round_data: dict) - None: # 实际项目中这里调LLM做摘要示例用简单拼接代替 new_piece f[用户说:{round_data[user]} 助手答:{round_data[assistant]}] if self.summary: self.summary f{self.summary}; {new_piece} else: self.summary new_piece def build_messages(self) - list[dict]: messages [] if self.summary: messages.append({role: system, content: f以下是更早的对话摘要当与最近对话冲突时以最近对话为准: {self.summary}}) for r in self.rounds: messages.append({role: user, content: r[user]}) messages.append({role: assistant, content: r[assistant]}) return messagesContextWindow有两个触发压缩的条件轮数超过max_rounds或者总token超过max_tokens。两个条件独立触发谁先到谁先压缩。_merge_into_summary在演示代码里用简单拼接生产环境里这里应该调用大模型做一句话概括并且必须保留“用户诉求”和“最终结论”两个要素别把过程细节留进摘要。build_messages里有个容易忽略的设计摘要放在 system 角色而且明确写了一句“当与最近对话冲突时以最近对话为准”。这句话是血泪经验换来的——LLM在同时看到摘要和原文时经常搞不清时间先后导致被旧摘要误导。有了这句显式指令模型在冲突时会优先采信尾部原文。3.3 指代消解的一个务实做法用“最近N轮实体表”做全局代词替换指代消解是上下文推理里最容易被低估的问题。用户说“它”“那个”“这个”模型如果不知道指什么整轮对话就断了。学术圈有一套复杂的指代消解模型但对大多数业务对话场景来说一个“最近N轮实体表”就能解决八成问题而且可解释性更强。思路很简单每一轮从用户文本和历史上下文里抽出实体商品、订单号、服务类型等放进一张会话级的实体表当检测到当前轮出现高置信度代词时直接从实体表里取最近匹配的实体替换。下面是一个简化实现class MentionResolver: 基于最近N轮实体表的指代消解 def __init__(self, max_entities: int 5): self.entities: list[dict] [] # [{entity: 充电宝, type: 商品, time: 轮次}] self.max_entities max_entities def add_entities(self, new_entities: list[dict], current_round: int) - None: 每轮语义理解后把新实体追加进表尾 for ent in new_entities: ent[time] current_round self.entities.append(ent) # 只保留最近N个位置靠后的优先级更高 self.entities self.entities[-self.max_entities:] def resolve(self, user_text: str, current_round: int) - str: # 高置信度代词表 pronouns [它, 这个, 那个, 这单, 那单] resolved user_text for p in pronouns: if p in resolved: # 取时间最近且类型匹配的实体 for ent in reversed(self.entities): if ent[time] current_round: resolved resolved.replace(p, ent[entity]) break return resolved resolver MentionResolver() resolver.add_entities([{entity: 充电宝, type: 商品}], current_round1) resolved resolver.resolve(它坏了怎么办, current_round2) print(resolved) # 输出充电宝坏了怎么办这个实现有一个关键设计实体表按时间排序reversed遍历保证“最近提到的实体”优先被选中。max_entities5是一个合理默认值因为对话里的活跃实体通常不会超过5个太少会丢太多会让旧实体干扰新话题。但这里也有一个边界要守住当检测到用户明确引入新话题时比如提到一个不在实体表里的新商品名必须把实体表里匹配旧类型的那部分条目清掉否则“它坏了”会被错误地指向旧实体。我一般的处理方式是新实体加入时如果类型相同且表述不同就替换掉同名旧实体而不是追加。4. 上下文窗口里的重排与召回把“相关”放进推理前4.1 为什么多轮拼接后模型反而变“笨”信息混杂的机理就算做了滑动窗口你仍然会遇到一个困惑窗口只有最近8轮但模型回答质量还是不稳定。问题不在于窗口长度而在于这8轮里塞了太多“与当前问题无关的信息”。比如用户在第3轮问过商品价格第7轮才开始聊售后如果把第3轮的价格讨论和第7轮的售后诉求一起喂进去模型很容易在生成时混淆话题边界。从机制层面看自注意力网络对“远距离相关性”的捕捉能力是衰减的而且多段闲聊内容会在注意力矩阵里形成互相竞争的权重。你问“这个怎么收费”模型需要在本轮问题和第5轮的“会员服务介绍”之间建立关联但第2轮、第4轮那些无关讨论会把注意力分散掉导致模型抓错了重点。所以“把全部历史喂给模型”是下策“把最相关的历史选出来喂给模型”才是正解。这就需要一条独立的召回环节在每轮生成回答之前先用当前用户问题去历史记录里检索最相关的前几轮和当前问题拼在一起作为prompt。这条召回环节就是上下文推理和检索之间的桥梁。4.2 用语义相关性做历史轮次召回top-k重排的最简实现把历史轮次做成可检索的候选集每一轮包含“用户文本 助手回答 意图标签”三个字段。用户的当前问题输入后计算与每一轮的相似度取top-k输出。这里的相似度计算可以直接复用2.3节的text_embedding和cosine_sim不用额外引入检索服务。def recall_relevant_rounds(current_text: str, history: list[dict], top_k: int 3, threshold: float 0.62) - list[dict]: 从历史轮次里召回与当前问题最相关的top_k轮 # 对当前问题和每一轮的历史文本做向量化 cur_vec text_embedding(current_text) scored [] for i, h in enumerate(history): # 拼接该轮的用户文本和助手回答构成一个可检索单元 unit_text f{h[user]} {h[assistant]} h_vec text_embedding(unit_text) sim cosine_sim(cur_vec, h_vec) scored.append({index: i, sim: sim, round: h}) # 按相关度排序取前top_k scored.sort(keylambda x: x[sim], reverseTrue) results [s for s in scored[:top_k] if s[sim] threshold] # 按原始顺序返回不要打乱对话的时间线 results.sort(keylambda x: x[index]) return [s[round] for s in results]这个函数有两个容易被忽视的参数。第一top_k3不是越大越好我测试过4、5、6发现超过4之后召回进来的轮次里开始出现“语义相似但话题不同”的噪声对最终回答质量的提升是负向的。第二threshold0.62相对宽松因为历史轮次拼接了助手回答后和用户当前问题通常比较短之间的语义重合度天然不高阈值定太严会导致AI只有原文才能命中。排序之后必须按index恢复原始顺序这是另一个血泪教训。把召回的轮次打乱顺序放进prompt模型会分不清时间先后理解成两个并行的对话。你可以在prompt里加一句“以下是按时间顺序排列的对话历史”但顺序本身错了就补不回来。4.3 摘要化历史当轮数超过窗口时先压缩再拼接召回做完prompt里能放的原文轮数还是有限。如果用户真的聊了300轮就算召回了最相关的3轮模型仍然缺失了更早的关键信息。这时需要摘要化历史这个兜底手段。摘要不是把“所有轮次”压缩而是把“没有被召回的早期轮次”压缩成一段状态文本。我一般会触发两个条件才做摘要一是总轮数超过max_rounds且召回的top-k轮不足以覆盖关键约定二是用户的连续追问超过12轮此时原始轮次里的无效信息比例很高。摘要的内容固定包含四个要素用户的核心诉求、已经确认的关键信息、当前未解决的问题、之前承诺过的动作。下面是一个触发摘要的代码骨架def build_final_prompt(current_text: str, history: list[dict], dialog_state: DialogState, llm_summarize_fn) - list[dict]: # Step1: 从历史里召回相关轮次 relevant recall_relevant_rounds(current_text, history, top_k3) # Step2: 构造“未被召回”部分的摘要 relevant_indexes {id(r) for r in relevant} unrelevant [h for h in history if id(h) not in relevant_indexes] messages [] if unrelevant and len(history) 12: summary llm_summarize_fn(unrelevant, instructions保留用户核心诉求、已确认信息和未解决问题去掉寒暄和重复内容) messages.append({role: system, content: f旧对话摘要: {summary}}) # Step3: 结构化状态 召回轮次 当前问题 state_text dialog_state.get_context_prompt() messages.append({role: system, content: f对话状态: {state_text}}) for r in relevant: messages.append({role: user, content: r[user]}) messages.append({role: assistant, content: r[assistant]}) messages.append({role: user, content: current_text}) return messagesbuild_final_prompt的设计原则是“三层信息各司其职”摘要层提供背景状态层提供已确认事实召回层提供最近的相关上下文。值得留意的是摘要在前召回在后当前问题在末尾——这个顺序让模型在生成回答时优先看到“最近对话”再参考早期背景。我用这个结构替换掉之前“全量拼接”的方案后线上对话的跑题率明显下降而且token消耗只是原来的三分之一左右。5. 对话推理的常见坑与排查五条会让你在线上翻车的实操记录5.1 症状槽位被“昨天的内容”污染今天一问就答错现象用户今天问“寄到什么地址”系统直接用了三天前对话里留下的旧地址。原因是全局槽位表没有设置生命周期管理只要会话不销毁槽位就一直存活。更隐蔽的是用户换了一个话题旧槽位仍然留在表里。解决所有槽位增加时间戳和来源轮次标记每次更新时检查是否超过会话轮数上限或者话题漂移一旦detect_intent返回新意图且和旧意图语义距离较远就清空旧意图对应的槽位。明确话题切换时宁可全部重置也不要保留旧值。5.2 症状指代消解把“那个功能”替换成了完全无关的实体现象用户说“那个功能怎么收费”被系统替换成了一小时前提到过的“高级会员”但实际上用户指的是当前页面上的“云存储”。原因是实体表范围太宽没有按话题片段做隔离。解决实体表加入“话题分段”概念检测到意图变化时建立新的话题分区代词消解只扫描当前话题分区的实体表不去碰老话题。另一个保守策略是限定代词消解只在最近3轮内找候选“那个”跨了5轮以上就放弃消解宁可让模型根据上下文自己猜也不要强行替换错。5.3 症状改了prompt后线上效果反而更差现象把“你是智能客服”改成“你是售后专家”后回答是更专业了但用户问“你家有什么产品”时模型开始拒答因为它认为自己是售后专家。原因prompt指令的优先级冲突角色定义干扰了意图识别管道的结果而代码里没有对两套输出做一致性校验。解决必须建立回归测试集。我的做法是维护一组覆盖常见意图边界表达的最小用例每次改prompt前跑一遍用脚本自动对比输出是否仍然命中预期意图。这个用例集不用大30到50条足够但必须包含“换货”“退货”“物流”“售后”每个意图至少8条变体。5.4 症状语义向量相似度“感觉对但实际错”现象用户说“我想退款”向量召回返回了“退货办理流程”这条知识相似度0.82但用户情绪明显是投诉系统却给了一个标准流程回答。原因相似度衡量的是语义距离不是“用户意图和目标内容的匹配度”。向量不知道“退款”和“投诉”在业务上完全是两条处理路径。解决相似度之后必须加业务规则校验比如退款相关的槽位订单号、支付方式如果不能全部提取出来就不能直接进入退款流程知识回答而是转人工或者追问补全信息。向量只是在候选集里缩小范围最终决策仍然要由业务逻辑把关。5.5 症状上下文摘要与当前问题冲突回答自相矛盾现象用户在第5轮明确说“不要短信通知”但摘要把这句压缩成了“用户希望接收通知”第10轮系统建议开启短信提醒。原因摘要压缩时把否定表达丢了。大模型做摘要时天然倾向保留“行动型”信息而忽略“否定型”限定词。解决摘要指令里明确写出“必须完整保留否定词、时间词、数量词”并把摘要视为低置信度知识一旦与最近原文冲突以最近原文为准。另一层防线是对包含“不要”“别”“取消”这些否定词的槽位单独存储不进摘要直接作为全局状态的硬约束。6. 进阶技巧两段式召回加语义缓存把多轮对话的稳定性和成本一起优化到这一步语义理解、上下文推理、相关历史召回都已经是结构化模块了。最后一层优化我建议做两件事两段式召回和语义缓存。前者解决“召回质量”后者解决“重复计算成本”。两段式召回的第一段是关键词粗筛用BM25或者简单的词频匹配把历史轮次里包含当前问题关键词的轮次选出来第二段是向量精排在粗筛结果里做相似度排序。这样做比直接对全量历史做向量计算快得多而且粗筛能拦住一部分“词语完全不同但语义相近”的噪声。对线上对话系统来说召回延迟能压到50毫秒以内。语义缓存则是把“用户问题的向量表示”作为key把“模型回答”作为value存起来。当用户问的句子和过去某句的相似度超过0.95时直接返回缓存回答不再调用大模型。这个优化在客服场景里效果极明显因为用户反复问的永远是那三四十个问题。我实测过命中率能到20%到30%对应的大模型调用成本直接降两成。class SemanticCache: def __init__(self, cache_limit: int 500, threshold: float 0.95): self.data: list[dict] [] self.cache_limit cache_limit self.threshold threshold def lookup(self, query: str) - str | None: q_vec text_embedding(query) for item in self.data: if cosine_sim(q_vec, item[vec]) self.threshold: return item[answer] return None def store(self, query: str, answer: str) - None: self.data.append({query: query, vec: text_embedding(query), answer: answer}) if len(self.data) self.cache_limit: # 简单淘汰移除最旧的缓存项生产环境可以换成LRU self.data.pop(0) cache SemanticCache() cached cache.lookup(充电宝坏了怎么换) if not cached: # 调用模型... response 请提供您的订单号我们帮您安排换货。 cache.store(充电宝坏了怎么换, response)缓存阈值0.95看起来很高但这是故意的。语义缓存只缓存“用户几乎用同样话问过”的场景宁可不命中也不能因为缓存匹配错误给出过时回答。cache_limit建议根据线上规模设一般500到2000条足够覆盖高频问题。淘汰策略在演示里是先进先出生产环境我更推荐按最后命中时间做LRU这样“热门问题”能长期留在缓存里。技术栈做到这里完整链路已经跑通文本清洗 → 意图识别 → 槽位提取 → 混合决策 → 对话状态跟踪 → 指代消解 → 相关轮次召回 → 摘要压缩 → 语义缓存。这套东西我前后调了两个多月最大的教训是“不要相信任何一次性的效果判断”——语义理解的上限是由测试集覆盖度决定的不是由某一条prompt决定的。你的业务里那些高频问法值得花时间逐条塞进回归集比反复琢磨提示词有用得多。希望帮到你。本文还有配套的精品资源点击获取