首页 / 资讯中心 / 文章详情

从单次问答到多轮对话:ChatGPT类应用与LangChain实战指南

从单次问答到多轮对话:ChatGPT类应用与LangChain实战指南 ★ FEATURED ARTICLE
1. 从单次问答到多轮对话ChatGPT 类应用的能力跃迁很多人第一次接触 ChatGPT 这类对话式 AI都是从网页上那个输入框开始的敲一句话等几秒答案就出来了。用久了你会发现真正让人上瘾的不是单次问答而是它能记住你前面说过什么能顺着上下文继续聊。这个“记住上下文”的能力才是聊天机器人和普通搜索框的分水岭。我把它叫做对话式 AI 的第二层能力——第一层是“能答”第二层是“能接着聊”。这一篇要聊的就是围绕 ChatGPT 这类对话式 AI 和聊天机器人怎么把“能接着聊”这件事做扎实。核心会落在几个关键词上对话式 AI、聊天机器人、LangChain、提示工程。如果你正在琢磨怎么给自己的产品接一个能多轮对话的机器人或者想搞清楚 LangChain 这类框架到底解决了什么问题又或者你只是好奇那些“机器人无限制聊天软件”背后大概是怎么搭的这篇内容应该能给你一些能直接抄作业的思路。需要先说明一点我这里讲的不是某个具体产品的破解或绕过方法而是从工程实现的角度讲清楚一个多轮对话机器人从零到能用的核心环节。适合有基础编程概念、想动手做点东西的读者也适合产品经理、运营同学了解技术边界在哪里。全文会尽量用生活化的类比把原理讲透同时给出可复现的步骤和参数选择的理由。2. 对话式 AI 的整体设计与思路拆解2.1 为什么单次问答不够用上下文才是灵魂先想一个场景。你问机器人“帮我写一段 Python 代码读取 CSV 文件。”它给你一段代码。你接着说“改成读取 Excel。”如果它不记得上一句它根本不知道“改成”是改什么。这就是单轮问答的天花板——每一句话都是孤立的没有记忆没有指代消解没有话题延续。多轮对话要解决的核心问题就是把历史对话作为输入的一部分一起送给模型。听起来简单但工程上有一堆细节历史对话怎么存存多少太长怎么办不同用户的对话怎么隔离这些问题不解决机器人要么答非所问要么成本爆炸。我个人的经验是把多轮对话拆成三个层次来设计会话层负责管理“谁在跟谁聊”上下文层负责决定“这次带哪些历史进去”模型层负责“根据带进去的内容生成回复”。这三层分开设计后面换模型、换存储、换策略都不会牵一发动全身。2.2 方案选型自己撸还是用框架确定要做多轮对话之后第一个岔路口就是自己从零写还是用现成框架。我两种都试过说说各自的适用场景。自己从零写好处是可控。你清楚每一行代码在干什么调试的时候不会一头雾水。适合对话逻辑简单、只需要拼接历史消息的场景。比如你只是做一个内部工具用户量不大历史对话就是简单地按时间顺序拼起来那自己写几十行代码就够了。用框架典型代表就是LangChain。它把“加载历史、拼接提示、调用模型、解析输出”这一套流程抽象成了链Chain和记忆Memory组件。好处是当你需要接入向量数据库做长期记忆、需要做工具调用、需要多步推理的时候框架帮你省掉大量胶水代码。坏处是抽象层多了出问题的时候排查链路变长。我的建议是先用原生方式跑通一个最小可用的多轮对话理解每一步在干什么然后再决定要不要上框架。很多人一上来就 LangChain结果连消息是怎么拼进提示里的都没搞清楚遇到问题只能靠猜。这个顺序反过来学习曲线会平缓很多。2.3 提示工程在多轮对话里的角色提示工程这个词被说烂了但在多轮对话场景里它的作用非常具体告诉模型怎么使用历史信息。同样一段历史对话提示写得好和写得差输出质量差很多。举个实际的例子。假设历史里用户说过“我不吃辣”当前问题是“推荐几个菜”。如果系统提示里没有明确指示模型关注用户的偏好模型很可能推荐水煮鱼。但如果你在系统提示里加一句“请优先考虑用户在对话中提到的饮食限制”推荐结果就会明显不同。这里的关键认知是历史对话不是自动生效的它需要被“激活”。模型不会主动去翻历史找约束条件你得在提示里明确告诉它去看、去用。这是很多人做多轮对话时容易忽略的一点——以为把历史塞进去就行了其实还得告诉模型怎么用这些历史。3. 核心细节解析与实操要点3.1 对话历史的存储结构设计先讲存储。多轮对话的历史最直接的存法就是一个列表每个元素是一条消息包含角色和内容。角色通常有三种system系统提示、user用户、assistant机器人。这个结构看起来简单但设计的时候有几个点要想清楚。第一会话 ID 怎么生成和管理。每个独立的对话线程需要一个唯一标识通常用 UUID。这个 ID 要跟用户身份绑定但又要允许一个用户开多个对话线程。我的做法是用户表里存一个默认会话 ID每次新建对话就生成新的 UUID前端把当前会话 ID 带在请求里。第二消息的元数据。除了角色和内容我习惯再存几个字段时间戳、token 数量、模型版本。时间戳用于排序和过期清理token 数量用于控制上下文长度模型版本用于排查“为什么昨天答得好今天答得差”这类问题。这些字段平时用不上出问题的时候能救命。第三存储介质的选择。小规模用 SQLite 或 Redis 就够了大规模上 PostgreSQL 或 MongoDB。我实测下来对于日活几千的应用Redis 存活跃会话加定期落库到关系型数据库是一个成本和性能比较平衡的方案。Redis 的过期时间设成 24 小时超过一天没活动的会话就自动清理避免内存无限增长。3.2 上下文窗口的管理策略这是多轮对话最核心的技术点之一。模型的上下文窗口是有限的你不能把全部历史都塞进去。怎么在有限的空间里放最有用的信息直接决定对话质量。我常用的策略是滑动窗口加摘要。具体来说保留最近 N 轮完整对话更早的对话用模型生成一段摘要把摘要放在系统提示里。N 的取值取决于你的场景我一般设 5 到 10 轮。为什么是 5 到 10因为大多数对话的关键信息集中在最近几轮再往前的信息密度急剧下降。摘要的生成时机也有讲究。我试过每轮都重新生成摘要成本太高也试过固定间隔生成但可能错过关键信息。最后采用的方案是当历史消息的 token 总数超过阈值比如模型窗口的 60%时触发一次摘要生成把最老的一半消息压缩成一段文字。这样既控制了成本又保证了关键信息不丢。注意摘要本身也会消耗 token而且摘要是有损压缩。对于需要精确回忆的场景比如用户报过的订单号摘要可能会丢失细节。这类关键信息建议单独提取成结构化字段存起来不要依赖摘要。3.3 提示模板的设计要点提示模板是多轮对话的“剧本”。我一般把提示分成三块系统指令、历史对话、当前输入。系统指令定义机器人的角色、能力边界、输出格式历史对话提供上下文当前输入是用户这次说的话。系统指令的写法有几个心得。第一角色定义要具体。“你是一个助手”太泛“你是一个电商客服助手负责处理退换货咨询语气友好但简洁”就具体得多。第二约束条件要明确。比如“如果用户的问题超出退换货范围请引导用户联系人工客服”这种边界条件不写清楚模型就会自由发挥。第三输出格式要规定。如果你希望回复是 JSON就在系统指令里给出示例格式。历史对话的拼接顺序也有讲究。我试过正序和倒序最后发现正序从旧到新效果更稳定。原因是模型在训练时见到的对话数据大多是正序的倒序会让它有点“晕”。另外在历史对话和当前输入之间加一个明确的分隔标记比如“--- 以上是历史对话以下是用户新消息 ---”能帮助模型区分哪些是背景、哪些是要回应的。3.4 模型参数的选择逻辑调用模型时的几个关键参数temperature、max_tokens、top_p。这些参数怎么设直接关系到对话的稳定性和创造性。temperature 控制随机性。做客服机器人我一般设 0.2 到 0.5保证回答稳定、不跑偏。做创意写作助手可以设 0.7 到 1.0让输出更多样。我踩过的坑是temperature 设太高同一个问题问两次得到完全不同的答案用户会觉得机器人“不靠谱”设太低回答又太死板像在念模板。max_tokens 控制回复长度。这个要根据场景设。客服场景一般 200 到 500 就够了太长了用户没耐心看。但要注意max_tokens 是包含在上下文窗口里的设太大可能挤占历史对话的空间。我的做法是先估算历史对话的平均 token 数然后用窗口大小减去历史 token 数再留 20% 的余量剩下的才是 max_tokens 的上限。top_p 是另一种控制随机性的方式和 temperature 二选一用就行。我通常固定 temperature把 top_p 设为 1减少一个变量。4. 实操过程与核心环节实现4.1 最小可用的多轮对话实现先给一个不依赖任何框架的原生实现用 Python 写方便你理解每一步。这里用伪代码风格具体 SDK 调用按你用的模型服务调整。import uuid import time # 会话存储生产环境换成 Redis 或数据库 sessions {} def get_history(session_id): return sessions.get(session_id, []) def save_message(session_id, role, content): if session_id not in sessions: sessions[session_id] [] sessions[session_id].append({ role: role, content: content, timestamp: time.time() }) def build_prompt(session_id, user_input, max_turns10): history get_history(session_id) # 只取最近 max_turns 轮 recent history[-max_turns * 2:] messages [ {role: system, content: 你是一个友好的助手回答简洁准确。} ] for msg in recent: messages.append({role: msg[role], content: msg[content]}) messages.append({role: user, content: user_input}) return messages def chat(session_id, user_input): messages build_prompt(session_id, user_input) # 调用模型这里用伪函数表示 reply call_model(messages, temperature0.5, max_tokens500) save_message(session_id, user, user_input) save_message(session_id, assistant, reply) return reply这段代码的核心逻辑就三步取历史、拼提示、存新消息。跑通之后你可以逐步加东西加摘要、加向量检索、加工具调用。每一步加什么取决于你的场景需要什么不要为了用技术而用技术。4.2 用 LangChain 重构什么时候值得上框架当你需要下面这些能力时LangChain 这类框架的价值就体现出来了长期记忆向量数据库、工具调用让模型查天气、查数据库、多步推理Chain、多模型切换。如果只是简单的多轮对话原生实现更轻、更好调试。用 LangChain 实现多轮对话核心是ConversationBufferMemory或ConversationSummaryMemory。前者存全部历史后者自动生成摘要。我一般用ConversationSummaryMemory因为它自动处理了上下文长度的问题。from langchain.memory import ConversationSummaryMemory from langchain.chains import ConversationChain from langchain.llms import YourLLM memory ConversationSummaryMemory( llmYourLLM(), max_token_limit2000 ) conversation ConversationChain( llmYourLLM(temperature0.5), memorymemory, verboseTrue ) # 多轮对话 response1 conversation.predict(input我想学 Python) response2 conversation.predict(input应该从哪里开始)这里max_token_limit设 2000意思是历史摘要控制在 2000 token 以内。超过这个数框架会自动把更早的内容压缩。这个参数要根据你的模型窗口和业务需求调。我一般设成模型窗口的 30% 到 40%给当前输入和回复留足空间。4.3 提示工程实战让机器人记住关键约束前面说了提示工程的重要性这里给一个具体的模板示例。假设你要做一个餐饮推荐机器人用户可能在中途提到忌口。你是一个餐饮推荐助手。请根据用户的偏好推荐菜品。 重要规则 1. 如果用户在对话中提到任何饮食限制如忌口、过敏、素食 必须在推荐时严格遵守并在回复中确认你已注意到这些限制。 2. 推荐时给出菜品名称和一句话理由。 3. 如果用户没有提到限制推荐 3 个大众口味的菜品。 历史对话 {history} 用户新消息{input}这个模板的关键在于第 1 条规则。它明确告诉模型去历史里找饮食限制找到之后要遵守还要在回复里确认。我实测下来加了这条规则之后模型忽略用户忌口的概率从大概三成降到了不到一成。这就是提示工程的实际价值——不是玄学是具体的指令设计。4.4 参数计算上下文窗口怎么分配假设你用的模型上下文窗口是 4096 token。你要决定历史对话、系统提示、当前输入、回复各占多少。我的分配方案是这样的组成部分建议占比4096 窗口对应 token 数系统提示10%约 400历史对话40%约 1600当前输入20%约 800回复预留30%约 1200这个分配不是死的要根据场景调。客服场景历史对话占比可以高一些因为用户可能前面说了很多背景信息。创意场景当前输入占比可以高一些因为用户可能一次给很长的创作要求。计算 token 数不用太精确用“字符数除以 2”粗略估算英文中文大概“字符数除以 1.5”。更准的办法是用模型提供的 tokenizer但日常开发用估算就够了留足余量比精确计算更重要。5. 常见问题与排查技巧实录5.1 机器人“失忆”历史没带上或带错了这是最常见的问题。用户说“刚才不是说了吗”机器人一脸茫然。排查思路按这个顺序走第一检查会话 ID 是否一致。前端每次请求带的 session_id 是不是同一个我遇到过前端在页面刷新后重新生成 session_id 的情况导致历史全丢。解决办法是把 session_id 存在浏览器的 localStorage 里而不是每次生成。第二检查历史是否真的拼进了提示。在调用模型之前把完整的 messages 打印出来看一眼。很多时候是代码逻辑问题比如取了历史但忘了拼进去或者拼的时候顺序错了。第三检查历史是否被截断。如果 max_turns 设得太小或者摘要逻辑有 bug历史可能在你不注意的时候被丢掉了。建议在日志里记录每次请求实际带了多少条历史消息。5.2 回复越来越慢、越来越贵多轮对话跑一段时间后你会发现响应变慢、成本上升。原因通常是历史越积越多每次请求的 token 数越来越大。解决办法就是前面说的摘要加滑动窗口。但要注意摘要生成本身也要调模型也有成本。我的经验是摘要的触发频率要控制不要每轮都生成。设一个 token 阈值超过才触发。另外摘要用的模型可以用小一点的、便宜一点的因为摘要不需要太强的生成能力。还有一个容易被忽略的点清理不活跃会话。很多对话聊完就完了但历史一直存在那里占空间。设一个过期时间比如 24 小时或 7 天自动清理。这个策略要在产品层面想清楚因为有些场景用户可能希望历史永久保留。5.3 模型不遵守系统提示里的规则你明明在系统提示里写了“不要推荐辣菜”模型还是推荐了。这种情况通常有几个原因一是规则太多模型顾不过来。系统提示里的规则建议不超过 5 条太多了模型会“选择性执行”。如果规则确实多考虑分层核心规则放系统提示次要规则放当前输入里。二是规则表述有歧义。“不要推荐辣菜”不如“推荐的所有菜品必须是不辣的”明确。否定式指令模型容易搞混尽量用肯定式。三是历史对话里的信息覆盖了系统提示。比如用户在历史里说“我今天想吃辣”那模型推荐辣菜其实是对的。这时候要在系统提示里加一句“以用户最新偏好为准”。5.4 常见问题速查表现象可能原因排查动作机器人不记得前面说的话会话 ID 不一致 / 历史未拼入 / 被截断打印 messages 检查回复变慢变贵历史累积过多检查 token 数加摘要和过期清理不遵守系统规则规则太多 / 表述歧义 / 被历史覆盖精简规则改肯定式加优先级说明回复格式不对系统提示未规定格式 / 模型忽略在提示里给格式示例同一问题答案差异大temperature 过高降低 temperature 到 0.5 以下中文回答夹杂英文系统提示语言不明确明确写“请用中文回答”5.5 几个踩过的坑和独家技巧第一个坑不要把用户输入直接拼进系统提示。有些人图省事把用户输入格式化后塞进系统提示里。这会导致提示注入问题——用户可以在输入里写“忽略以上所有指令”然后模型就真的忽略了。正确做法是把用户输入作为独立的 user 消息和系统提示分开。第二个坑历史对话里的 assistant 消息不要重新生成。有些人为了“优化”历史把之前的 assistant 回复重新改写一遍再存进去。这会导致历史不一致模型会困惑。历史就是历史存什么就是什么。第三个技巧在系统提示里加一句“如果不确定请询问用户而不是猜测”。这一句话能显著降低模型胡编乱造的概率。我实测下来加了这句话之后模型在信息不足时主动提问的比例明显上升用户体验反而更好。第四个技巧给机器人设一个“记忆锚点”。在长对话里定期让模型总结一下“目前已知的关键信息”把这个总结放在系统提示里。这样即使历史被截断关键信息也不会丢。这个操作可以手动触发也可以设成每 N 轮自动执行一次。6. 从能用到好用多轮对话的进阶方向把基础的多轮对话跑通之后你会发现真正的挑战在于“好用”。什么叫好用用户不用重复说背景机器人能主动追问缺失信息长对话不跑题换话题能跟上。这些都需要在基础架构上继续加东西。一个实用的进阶方向是结构化记忆。除了存原始对话再额外提取一些结构化字段用户提到的偏好、约束、已确认的事实。这些字段单独存每次请求时作为系统提示的一部分注入。这样即使原始对话被摘要压缩了关键约束还在。实现上可以用模型做信息抽取把用户消息里的关键信息抽成 JSON 存起来。另一个方向是话题检测和切换。当用户从“退换货”聊到“优惠券”时机器人应该能识别话题变了调整回复策略。这个可以用简单的关键词匹配做也可以用模型做意图分类。我一般先用规则做一版跑一段时间收集数据再决定要不要上模型。还有一个方向是多轮对话的评估。怎么知道你的机器人对话质量好不好我常用的方法是构造一批多轮对话测试用例每轮检查机器人是否遵守了约束、是否引用了正确的历史信息。这个测试集不用很大几十条就能发现大部分问题。每次改提示或改参数跑一遍测试集看通过率有没有下降。这些进阶方向不需要一次全上根据你的实际需求选。我的建议是先把基础的多轮对话跑稳收集真实用户的对话数据看看问题主要出在哪里再针对性地加功能。技术方案永远是为场景服务的脱离场景堆技术最后只会得到一个复杂但不好用的东西。我个人在实际操作中的体会是多轮对话的难点不在模型而在工程细节。历史怎么存、怎么取、怎么拼、怎么清理这些看起来琐碎的事情才是决定机器人好不好用的关键。模型能力再强历史没带对照样答非所问。所以如果你刚开始做别急着上框架、上向量数据库先把最基础的消息拼接和会话管理做扎实后面加什么都会顺很多。
阅读完成 · 觉得有帮助?
咨询建站