用 Java 接入大模型(五):多轮对话的上下文怎么管

简介: 本文深入剖析多轮对话的核心误区:大模型本身无记忆,所谓“遗忘”实为上下文管理失当。从messages结构化封装、token精准截断策略,到Redis会话存储与三步排错法,系统讲解如何可靠构建长程对话能力。

客户端封装好了,接下来要往里面装业务。多轮对话是最典型的场景,也是最容易做错的一个。很多人做出第一版之后会遇到同一个现象:聊到十几轮,模型开始"忘了前面说过什么"。这时候第一反应是模型不行,但原因通常在自己这边——上下文没管好。这一篇把这个场景从头拆开。


一、先接受一件事:模型不记得你

所有多轮对话的困惑,都源于一个没被讲清楚的前提:大模型接口是无状态的。

不是"记性不好",是根本不记。你发过去的请求,对模型来说就是一次性的:给它一段文本,它接着往下写,写完就忘。下一次请求,除非你把之前的内容再发一遍,否则它完全不知道你们聊过什么。

这个前提听起来很基础,但它解释了一连串现象:

  • 为什么每次请求都要把历史全带上?
  • 为什么聊得越久,每次请求越慢、越贵?
  • 为什么"它明明刚才说过"这种抱怨,本质上是个伪命题?

所谓"多轮对话",其实是客户端每次把完整的历史重新发一遍,让模型看起来像是有记忆。记忆不在模型那边,在你这边。

所以多轮对话的本质工作,不是"让模型记住",而是"决定每次带多少历史过去"。这一篇剩下的内容都是围绕这一句展开的。


二、别提字符串,用 messages 数组

看两种典型写法。

第一种,自己拼字符串:

String prompt = "用户:" + q1 + "\n助手:" + a1 + "\n用户:" + q2;

第二种,用结构化的 messages:

List<Map<String, Object>> messages = List.of(
    Map.of("role", "system",    "content", "你是一个技术支持助手"),
    Map.of("role", "user",      "content", q1),
    Map.of("role", "assistant", "content", a1),
    Map.of("role", "user",      "content", q2)
);

第一种能跑,但问题会陆续冒出来:

  • 用户在问题里打一句"助手:好的",历史就被伪造了。模型分不清哪句是它自己说的。这是那种平时遇不到、一旦被恶意用户发现就很难看的漏洞。
  • 你要做截断、要按消息数限制、要插入工具调用结果,都得在字符串上做正则,很快就没法维护。
  • 一些新能力(工具调用、多模态)根本没法用字符串表达。

所以没有理由用字符串拼接。用 messages 数组,这是接口本身规定的形式,不是风格偏好。

三个角色的分工要记牢:

  • system:设定身份、约束和规则,放最前面,每次请求都带;
  • user:用户说的话;
  • assistant:模型之前的回答,从上一轮响应里拿到的原文。

assistant 的内容要用模型返回的原文,不要自己改写或截断。改写会污染后续上下文,而且改出来的内容会让模型对自己之前说过的话产生误判。


三、上下文窗口:一个共享的额度

每次请求,输入和输出共享一个 token 上限,这就是上下文窗口。

这一点很关键,因为它意味着你带的输入越多,能留给输出的空间就越少。极端情况是:历史塞得太满,模型只剩下几十个 token 的输出空间,回答还没说完就被截断——表现就是"它突然不说话了"或者"话说到一半停了"。

所以带多少历史,不能只看"能塞多少",要给它留出输出空间:

可用输入额度 ≈ 上下文窗口 − max_tokens − 安全余量

安全余量必须留。原因有两个:一是 token 的估算很难精确(中文一个字往往不是一个 token,代码和标点更不规律),二是有些模型实现会在内部加一些额外的结构 token,你的估算里没算进去。留出 10%~15% 的余量,比事后排查"为什么偶尔报超长"要省事得多。

顺带一个现实提醒:不同模型判断超长的行为不一样。 有的直接报错返回,有的悄悄从中间截掉一段(这种情况最坑,你完全不知道历史缺了一块),有的会拒绝。所以别依赖报错来发现问题,要在自己这边主动控制长度。


四、超长了丢哪部分

这是多轮对话里最需要想清楚的一个决策。历史必然有一天会超出额度,那时候丢掉什么、保留什么,直接决定对话质量。

绝对不能丢的

system 消息。 它是整段对话的规则来源,丢了之后模型的行为会突然变得不受约束,出现完全不遵守指令的情况。截断逻辑里必须把 system 排除在外。

最近几轮对话。 用户当前的问题往往依赖前面的语境,"那它呢?"这种问题离开上一轮就没法回答。所以尾部要完整保留。

三种常见策略

策略一:滑窗(保留最近 N 轮)

最简单,把最老的轮次整对删掉,直到长度够。

// 保留 system + 最近 N 轮(一轮 = 一问一答)
List<Map<String, Object>> trimmed = new ArrayList<>();
trimmed.add(systemMessage);
int from = Math.max(1, messages.size() - N * 2);
trimmed.addAll(messages.subList(from, messages.size()));

优点是实现简单、可预测。缺点是丢历史是硬丢失——用户在前面提过的关键信息,一旦滑出窗口就彻底没了,模型会再问一遍"请问您说的是哪个订单"。长对话里这个体验很糟。

策略二:摘要压缩

当历史超过阈值时,把最早的一段交给模型,让它压缩成一段摘要,然后用这段摘要替换原来的多轮内容。

好处是信息密度高,几十轮能压成几百字,还保住了早期提到的关键事实。代价是多了一次调用(要花钱、要花时间),而且摘要会丢细节,压缩过程中的信息损失不可控。

实践上比较稳的用法是混合:保留 system + 摘要 + 最近 N 轮。这样既有远期记忆,又有精确的近期语境。

策略三:按重要性挑

把历史里的关键信息(用户给的订单号、报错信息、时间范围)单独抽出来,作为一条附加说明保留。

这个做法效果好,但需要针对业务定制,通用性差。适合对话内容比较固定的场景,比如客服。

一个必须处理的边界

真正动手写截断时,有个地方特别容易出问题:别把成对的消息拆散。

具体说三件事:

  • user 和 assistant 要成对。切在中间会出现连续两条 user,或者历史以 assistant 开头。有些模型对这种结构会直接报错,有些则不报错但回答质量明显下降——后者更难排查。
  • 工具调用和它的返回必须在一起。如果你用了 Function Calling,一条 assistant 消息里带着 tool_calls,紧接着必须是对应的 tool 结果消息。从中间截断会让结构不合法,接口直接拒绝。
  • 要用 token 数判断,不要按消息条数判断。消息条数相同,内容长度可能差十倍。按条数截断必然出现"有时候没超,有时候超了"的不稳定现象。

判断超长时,token 数尽量用模型厂商提供的计数接口或 SDK 来算,不要自己按字符数乘个系数估——中文、代码、标点、emoji 的差异很大,估出来的值会系统性偏移。


五、会话状态存在哪

上面讲的都是"怎么拼上下文",但还有一个前置问题:这些历史存在哪里?

内存

用 Map<String, List<Map<String,Object>>> 存,会话 ID 做 key。

适合单机演示。上生产会同时踩两个坑:

  • 重启就丢。用户发完一条消息,你这边重启了一下,历史全没了,模型立刻"失忆"。
  • 多实例不共享。部署了两个实例,用户的下一条请求被负载均衡打到另一台,历史是空的。表现就是"聊着聊着突然忘了",而且是随机发生的,很难复现。

如果你的服务是多实例部署(绝大多数生产环境都是),内存方案直接排除。

Redis

把 messages 序列化后存 Redis,用会话 ID 做 key,设置过期时间(比如 30 分钟无活动就回收)。

这是多数场景的合理选择:多个实例共享、天然支持过期、读写快。要注意的几点:

  • 必须设过期时间。否则废弃的会话会一直堆着,内存慢慢被吃掉。
  • 加锁防止并发写坏历史。同一个用户快速连发两条消息,两个请求会同时读到同一份历史、各自追加、各自写回,后写的把先写的覆盖掉——用户会发现自己说的一句话"没被听见"。用分布式锁或者按会话串行化来避免。
  • 控制存储大小。一个会话的历史可能占几十 KB,量大了要评估内存成本,必要时压缩后存。

数据库

要把对话留档(审计、数据分析、客服质检)就落库。但别把库当上下文存储用——每次都从库里查全量历史再拼,数据量上来之后读写都会成为瓶颈。

更合理的分工是:库负责留档,Redis 负责上下文。每次对话结束后异步落库,上下文只从 Redis 读写。两边职责分开,谁都不为难。


六、"忘了"的问题,先查这三处

聊到十几轮之后出问题,按这个顺序排查基本能定位:

第一处:历史根本没传全。 打印一次实际发出的 messages 数组,看到底带了几轮。很多时候是拼接逻辑有问题——比如每次都只带了最后一条,或者历史被某个地方覆盖了。这个方法最土,但命中率最高。

第二处:被截断截掉了。 看截断阈值和实际历史长度。如果 token 计数用的是自己估的系数,很可能估算偏低,导致实际请求超长,然后被模型那边悄悄截掉中间一段——你这边完全无感。

第三处:会话串了。 多实例部署 + 内存存储 + 会话 ID 复用(比如用了用户 ID 而不是会话 ID),就会出现两个用户的对话混到一起。这种问题看起来像"模型疯了",实际是数据串了。检查和会话 ID 的生成规则,以及是否存在多实例共享问题。

有一类现象要特别区分开:模型回答变得敷衍、反复问同样的问题、忽略 system 里的约束——这三种往往是上下文过长导致的退化,不是模型能力问题。上下文塞得越满,模型对中间部分的注意力越弱,这是已知的普遍现象。看到这种症状,先怀疑上下文太长,而不是先换模型。


七、几个容易忽略的实践细节

  • 历史里的消息要按时间顺序,别交错。并发写坏的典型症状就是顺序乱了。
  • 空的 assistant 响应不要进历史。如果上一轮因为超时或过滤返回了空内容,把它塞进历史会让模型困惑。宁可跳过这一轮。
  • 用户输入要先做长度限制。一个用户粘贴了十万字的文档进来,一次就把额度占满。在入口处就拦掉或分段,别等到底层报错。
  • 上下文长度会影响成本,不是只影响效果。第二轮请求带一轮历史,第十轮带九轮历史——每次请求的输入 token 是递增的。长对话里输入成本会明显超过输出成本。做成本估算时别只算输出。
  • 把"最近 N 轮"做成可配置项。不同业务差别很大,客服可能要 10 轮,翻译工具 1 轮就够。写死了后面一定要改。

八、小结

多轮对话的核心可以收成一句话:模型不记得你,记得的人是你。

所以这件工作的实质是每次请求前做一次取舍——带多少历史过去、丢掉哪些、怎么保证带的内容结构合法、存在哪里、并发下怎么不被写坏。这些决策都有明确的取舍代价,没有"标准答案",但都有明确的错误做法。

一个有用的自检方式:把每次实际发出的 messages 数组完整打出来看一眼。 很多"模型不行"的抱怨,在看到那串真实数据的那一刻就解释清楚了。


下一篇

阶段一到这里就结束了:能连上、知道该不该流式、参数会调、客户端能复用、上下文管得住。接下来的问题是,模型稳定返回的格式能不能被程序可靠地解析——也就是结构化输出。这件事看着简单,但"偶尔不是合法 JSON"这种情况,会让下游的解析全部失守。下一篇讲怎么让它稳定。


本文是「用 Java 接入大模型」系列第五篇。前四篇分别讲了流式输出、流式与非流式的取舍、参数调优、客户端封装,建议按顺序看。文中做法基于通义千问的 OpenAI 兼容接口,具体行为以你使用的模型文档为准。

目录
相关文章
|
24天前
|
人工智能 JSON 前端开发
从零用 Spring AI 搭建 RAG + Tool Calling 岗位分析系统:全流程实战与踩坑记录
本文记录使用Spring AI Alibaba+通义千问,从零构建“岗位分析+转岗建议”RAG+Tool Calling系统全过程:涵盖流式输出、结构化响应、知识库检索、薪资工具调用与Advisor编排,并详述5大实战坑点及解决方案,践行“AI不裁员,只赋能”的人文技术理念。
|
7天前
|
缓存 BI 测试技术
2026通义千问Qwen3.7-Max全解:Agent智能体底座,架构拆解与API实操教程
随着智能体应用快速落地,面向长周期自主任务的大模型成为开发选型的重点,新版通义千问Qwen3.7‑Max作为旗舰文本大模型,主打Agent智能体底座能力,在复杂推理、工程代码开发、超长文档处理、多轮工具调用等方面进行大规模优化,能够支撑长时间连续自动化任务。很多开发者在初次接触该模型时,容易混淆它与同系列其他版本的能力差异,不清楚MoE混合专家架构的运行逻辑,在API接入、工具调用编排时遇到参数配置、返回格式异常等问题。本文将从核心功能、底层技术架构、实操使用方法三个维度完整解析Qwen3.7‑Max,附带可直接运行的API调用代码,讲解参数配置方法,梳理开发过程中的限制条件与避坑要点,帮助开
269 2
|
28天前
|
前端开发 安全 中间件
《AgentScope 2.0实战指南》01 - 国内项目数最多的Agent框架
《AgentScope 2.0实战指南》是面向国产主流Agent框架的入门到生产落地教程,聚焦国内项目数最多的AgentScope 2.0——阿里通义实验室开源的生产级多智能体平台。全系列11节实战,从模型接入、工具调用、RAG知识库、跨会话长期记忆(ReMe)到带Web界面的完整问答助手,覆盖安全、高效、灵活、完备四大核心能力,助你零基础构建企业级智能体应用。
662 1
|
3月前
|
人工智能
Qwen3.8抢先体验!正式版即将发布并开源!
千问Qwen3.8即将开源,参数达2.4T,进化速度以“天”计,实力媲美Fable 5。预览版Qwen3.8-Max已上线阿里Token Plan等平台,限时优惠:日间Credits低至1折,夜间更优,个人/团队版月付仅35元起!
5577 166
|
18天前
|
人工智能 前端开发 API
阿里云百炼api入口官网:前台和后台,免费领超7000万Tokens
阿里云百炼是其AI大模型服务平台,提供前台官网(介绍、计费、领券)与后台控制台(API Key管理、Token用量查询、模型配置)。新用户可免费领取超7000万Tokens(每模型100万),支持按量付费或订阅计划降本。在阿里云百炼官网:https://t.aliyun.com/U/fPVHqY 免费领取千万Tokens
|
21天前
|
存储 自然语言处理 中间件
《AgentScope 2.0实战指南》09 - RAG 检索增强生成
《AgentScope 2.0实战指南》第9章详解RAG服务化:将私有文档(PDF/Word/Markdown等)经解析、切块、向量化后构建可检索知识库,通过RAGMiddleware无缝接入Agent,实现“所问即所得、答案可溯源”的生产级增强问答。代码开箱即用,支持多租户与动态更新。(239字)
231 0
|
19天前
|
存储 调度 开发工具
AgentScope深度解析:原生适配大模型的多智能体开发工具,标准化智能体交互探索24.4
AgentScope是阿里开源的轻量级多智能体开发框架,聚焦多Agent通信与协同,提供标准化消息模型、分布式支持、环境解耦及全链路调试能力,显著降低复杂任务编排与规模化部署门槛。
237 2
|
19天前
|
数据采集 人工智能 搜索推荐
GEO和SEO到底有什么区别? 看了就知道
GEO与SEO本质不同:SEO优化搜索引擎,抢关键词排名;GEO优化AI大模型,争答案引用权。前者让用户“找得到”,后者让AI“信得过、提得到”。二者非替代,而是基建(SEO)与信任(GEO)的协同关系。(239字)
199 1
|
2月前
|
Java 测试技术 Linux
JMeter下载、安装、汉化、插件配置、压测一篇搞定(附官网安装包)
Apache JMeter 是 Apache 基金会推出的免费开源性能测试工具,支持跨平台(Windows/macOS/Linux),可模拟高并发用户对Web/接口进行压力测试,具备功能全面、插件生态丰富、学习成本低等优势,是全球最主流的压测工具之一。(239字)

热门文章

最新文章