ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多轮对话长上下文:增量摘要与结构化摘要的 JSON 落地示例

多轮对话长上下文:增量摘要与结构化摘要的 JSON 落地示例 1. 多轮对话长上下文为什么会把上下文窗口撑爆多轮对话长上下文管理说白了就是让一个聊天机器人记住几十轮之前聊过什么同时别把模型的上下文窗口塞满。增量摘要和结构化摘要是解决这个问题的两种互补手段增量摘要负责把旧对话滚动压缩成一段短文本结构化摘要负责把关键信息抽成 JSON 字段让程序能直接读取。适合谁适合正在做 AI 编程助手、客服机器人、Agent 工具链的开发者尤其是那些发现对话到第 20 轮就开始变傻、或者 token 账单突然飙升的人。我试过最朴素的做法把全部历史消息一股脑塞进 messages 数组。前 5 轮没问题到第 15 轮请求体已经 3 万多 token模型开始忽略中间的内容回答质量断崖式下跌。更麻烦的是很多模型的上下文窗口虽然有 128K但实际有效注意力集中在首尾中间部分等于白花钱。于是自然想到截断。截断有两种极端一种是只保留最近 N 轮缺点是用户第 3 轮说过的偏好全丢了另一种是每轮都做摘要缺点是摘要本身也要调模型成本和延迟都上去了。真正工程上能落地的方案是分层处理最近几轮保留原文稍早的对话做增量摘要更早的对话做结构化摘要并合并。这里有个关键认知摘要不是一次性动作而是一个持续维护的状态。你需要一个 summary 变量每次新对话进来判断是否触发摘要触发后把「旧摘要 新对话」合并成「新摘要」。这个合并过程如果只用纯文本信息会随着轮次增加不断稀释如果用 JSON 承载就能做到字段级更新比如 pending_questions 字段每次被新摘要覆盖code_snippets 字段做去重追加。我踩过的坑是早期用纯文本摘要到第 30 轮时摘要里已经找不到用户最初说的「我用的是 Python 3.11不要给我 3.8 的写法」。因为每次合并模型都会重新概括细节被反复磨平。换成结构化 JSON 后key_facts 字段用集合去重这条信息就一直保留着。所以这篇文章要解决的问题很具体给你一套可复制的摘要提示词模板、一套 JSON 字段结构、一段能跑的多轮测试代码让你能自己验证摘要压缩率和信息保留度。压缩率就是摘要 token 数除以原始对话 token 数信息保留度就是关键事实在摘要后还能不能被正确召回。这两个指标决定了你的长上下文方案到底能不能上生产。2. TaoToken 前置把模型调用统一成 OpenAI 格式在写摘要逻辑之前得先把模型调用这层搞定。因为增量摘要和结构化摘要都要频繁调模型如果每家供应商的 SDK 不一样代码会写得很乱。我的做法是统一走 OpenAI 兼容格式base_url 指向 TaoToken 的 API 地址这样换模型只需要改一个 model 名字。TaoToken 在这里的角色是模型接入层。你可以在它的模型对话页面先手动试几轮确认摘要提示词的效果再落到代码里。官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数直接填进 base_url 就行。具体要准备三样东西Base URL、API Key、Model ID。Base URL 填 https://taotoken.net/api API Key 在控制台的 API Keys 页面生成Model ID 根据你的场景选摘要这种任务用轻量模型就够比如 gpt-4o-mini 或者 qwen 系列的小参数版本。如果你后面要做长期编码 Agent可以考虑 Coding Plan但摘要阶段先用按量调用验证逻辑。环境变量配置如下这是最省事的方式代码里不用硬编码密钥export OPENAI_API_KEY你的 TaoToken API Key export OPENAI_BASE_URLhttps://taotoken.net/api如果你用 Pythonopenai 库会自动读取这两个环境变量。注意 OPENAI_BASE_URL 结尾不要带斜杠否则某些版本会拼出双斜杠导致 404。我第一次配的时候就是多写了个斜杠报了一晚上的 connection error后来把 base_url 打印出来才发现。模型选择上摘要任务对推理能力要求不高但对指令遵循要求高因为你要它严格输出 JSON。建议选支持 JSON mode 或者 function calling 的模型这样解析成功率会高很多。如果模型不支持 JSON mode就在提示词里强调「只输出 JSON不要其他文字」并且在代码里做容错解析。还有一点摘要调用和主对话调用可以用不同的模型。主对话用能力强的模型保证回答质量摘要用便宜快的模型控制成本。这个分离很重要因为摘要调用频率高如果都用大模型账单会很难看。我在控制台里建了两个 Key一个给主对话一个给摘要方便分别看用量。3. 可复制的 JSON 摘要结构与提示词模板这一节是核心直接给你能抄的配置。先说 JSON 字段结构这是结构化摘要的骨架。字段设计的原则是程序能直接读的放结构化字段需要人看的放文本字段两者结合。{ topic: 讨论的核心主题一句话概括, code_snippets: [ 用户或助手分享的重要代码片段保留原始缩进 ], decisions_made: [ 已经确定的决策或方案比如选用了 asyncio ], pending_questions: [ 尚未解决的问题下一轮需要继续追问 ], key_facts: [ 重要事实比如用户环境、版本号、偏好 ], user_preference: { language: Python, style: 简洁给可运行代码 } }这个结构里code_snippets 和 key_facts 用数组合并时做去重pending_questions 每次被新摘要覆盖因为旧问题要么解决了要么过期了decisions_made 做追加因为决策是累积的。user_preference 是嵌套对象适合放稳定的用户画像。接下来是摘要提示词模板。这个模板我改了很多版关键是明确告诉模型「只输出 JSON」并且给出字段说明SUMMARY_PROMPT 请将以下编程对话提炼为结构化 JSON 摘要。 对话内容 {conversation_text} 请严格按以下 JSON 格式输出不要输出任何其他文字、不要用 markdown 代码块包裹 {{ topic: 讨论的核心主题一句话, code_snippets: [重要代码片段保留缩进], decisions_made: [已确定的决策或方案], pending_questions: [尚未解决的问题], key_facts: [重要事实如环境、版本、偏好], user_preference: {{language: , style: }} }} 要求 1. code_snippets 只保留完整可用的片段不要保留半截代码 2. key_facts 要具体比如Python 3.11而不是较新版本 3. pending_questions 只保留真正未解决的已解决的不要放 注意 JSON 示例里的花括号要写成双花括号因为这是 Python 的 f-string 格式。如果你用 .format() 或者字符串拼接就不用转义。这个细节坑过很多人报 KeyError 的时候先检查这里。合并逻辑也要写清楚。新旧摘要合并时不同字段策略不同def merge_summaries(old: dict, new: dict) - dict: return { topic: new.get(topic) or old.get(topic), code_snippets: list(dict.fromkeys( old.get(code_snippets, []) new.get(code_snippets, []) ))[-5:], decisions_made: old.get(decisions_made, []) new.get(decisions_made, []), pending_questions: new.get(pending_questions, []), key_facts: list(dict.fromkeys( old.get(key_facts, []) new.get(key_facts, []) )), user_preference: {**old.get(user_preference, {}), **new.get(user_preference, {})} }这里用 dict.fromkeys 做去重同时保持顺序比 set 好因为 set 会打乱顺序。code_snippets 只保留最后 5 个防止无限增长。pending_questions 直接覆盖因为新摘要反映的是当前状态。如果你用 Claude Code 做开发可以把这套结构写进项目的 CLAUDE.md 或者 settings 文件里让 Agent 知道摘要的字段约定。Cline 的 MCP 配置里也可以挂一个摘要服务但注意别直连生产数据库摘要数据放本地或独立存储。Codex 的 auth.json 里配置好 base_url 和 key 后模型调用就走统一入口了。4. 多轮样例验证压缩率与信息保留度实测光有结构不够得跑起来看效果。这一节给你完整的测试代码和预期输出。测试场景是编程助手用户从「想写并发下载器」开始经过选型、写代码、处理超时重试、内存问题一共 8 轮对话每 4 轮触发一次摘要。先看主类结构import os import json from openai import OpenAI class SummaryAssistant: def __init__(self, modelgpt-4o-mini): self.client OpenAI( api_keyos.environ[OPENAI_API_KEY], base_urlos.environ[OPENAI_BASE_URL] ) self.model model self.full_history [] self.recent_window [] self.structured_summary { topic: None, code_snippets: [], decisions_made: [], pending_questions: [], key_facts: [], user_preference: {} } self.summary_threshold 4 self.stats {raw_tokens: 0, summary_tokens: 0}触发摘要的判断逻辑当 full_history 长度达到阈值倍数时把超出部分拿去摘要。注意 user 和 assistant 消息成对计算所以阈值要乘 2。def _maybe_summarize(self): if len(self.full_history) self.summary_threshold * 2: return to_summarize self.full_history[:-self.summary_threshold * 2] if not to_summarize: return text \n.join(f{m[role]}: {m[content]} for m in to_summarize) self.stats[raw_tokens] len(text) prompt SUMMARY_PROMPT.format(conversation_texttext) resp self.client.chat.completions.create( modelself.model, messages[ {role: system, content: 你是技术摘要助手只输出合法 JSON。}, {role: user, content: prompt} ], temperature0.3 ) raw resp.choices[0].message.content.strip() raw raw.replace(json, ).replace(, ).strip() try: new_summary json.loads(raw) except json.JSONDecodeError: new_summary {topic: 编程讨论, code_snippets: [], decisions_made: [], pending_questions: [], key_facts: [], user_preference: {}} self.structured_summary merge_summaries(self.structured_summary, new_summary) self.stats[summary_tokens] len(json.dumps(self.structured_summary, ensure_asciiFalse)) self.full_history self.full_history[-self.summary_threshold * 2:]构建上下文时把结构化摘要转成 system 消息注入def _build_context(self, user_input): self.full_history.append({role: user, content: user_input}) self.recent_window.append({role: user, content: user_input}) self._maybe_summarize() context [{role: system, content: 你是编程导师擅长 Python 和系统设计。}] s self.structured_summary if s.get(topic): parts [f[历史摘要] 主题: {s[topic]}] if s[code_snippets]: parts.append(f重要代码: {s[code_snippets][:2]}) if s[decisions_made]: parts.append(f已定方案: {s[decisions_made]}) if s[pending_questions]: parts.append(f待解决: {s[pending_questions]}) if s[key_facts]: parts.append(f关键事实: {s[key_facts]}) context.append({role: system, content: \n.join(parts)}) context.extend(self.recent_window[-self.summary_threshold * 2:]) return context跑测试的时候8 轮对话在第 4 轮和第 8 轮各触发一次摘要。实测下来原始 8 轮对话约 4200 字符结构化摘要约 380 字符压缩率约 9%。信息保留度方面用户说的「Python 3.11」「不要用 threading」「文件很大要流式写入」这三条关键事实在摘要的 key_facts 里都能找到。验证信息保留度有个简单方法摘要后问模型「用户之前说过什么环境限制」看它能不能答对。如果答不出来说明 key_facts 字段没抽好需要调整提示词。我建议把「用户明确说的约束」单独列一个字段比混在 key_facts 里更可靠。压缩率不是越低越好。压到 5% 以下细节基本丢光10% 到 20% 是比较健康的区间。如果发现压缩率异常低检查是不是 code_snippets 把大段代码都存进去了代码片段要限制长度超过 500 字符的截断。5. 常见报错排查401、JSON 解析失败、OAuth 问题这一节列几个真实会遇到的报错以及怎么定位。第一个是 401 Unauthorized。报错长这样openai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}原因通常是 API Key 没配对环境变量或者 Key 复制时带了空格。检查方法在 Python 里打印 os.environ.get(OPENAI_API_KEY)[:8]看前几位对不对。如果用的是 TaoToken 的 Key确认是在 API Keys 页面生成的不是控制台登录密码。还有一种情况是 base_url 配错了请求打到了别的服务也会返回 401。第二个是 JSON 解析失败json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)这通常是模型输出里带了 markdown 代码块标记或者前面有「好的以下是摘要」这类废话。解决方法是解析前先清洗raw resp.choices[0].message.content.strip() if raw.startswith(): raw raw.split(\n, 1)[1] raw raw.rsplit(, 1)[0] raw raw.strip()更稳的做法是用模型的 JSON mode在请求里加 response_format{type: json_object}但前提是模型支持。如果不支持就在 system 消息里强调「只输出 JSON」。第三个是 local proxy failed 或者 connection error。这个多半是 base_url 写错或者网络环境有问题。先确认 base_url 是 https://taotoken.net/api 然后用 curl 测一下连通性curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $OPENAI_API_KEY \ -H Content-Type: application/json \ -d {model:gpt-4o-mini,messages:[{role:user,content:hi}]}如果 curl 能通但 Python 不通检查是不是代码里 base_url 多写了 /v1openai 库会自动补 /v1重复了会 404。第四个是 reading choices 报错比如AttributeError: NoneType object has no attribute choices这通常是响应结构和你预期的不一样可能是模型返回了错误信息但没抛异常。打印完整响应看看print(resp.model_dump_json(indent2))还有一种情况是流式响应没处理完就取 choices非流式调用不会有这个问题。如果你用 Claude Code 或者 Cline遇到 OAuth 相关报错检查 auth.json 或者 MCP 配置里的 base_url 和 key 是否和 TaoToken 控制台一致。三件套 Base URL、Key、Model ID 缺一不可Model ID 写错会报 model not found。6. 把摘要逻辑接进你的项目最后说落地。这套摘要逻辑可以直接嵌进任何 OpenAI 兼容的对话循环里。关键是把「摘要状态」当成会话的一部分持久化比如存 Redis 或者本地 JSON 文件下次会话恢复时读回来。如果你要做长期编码 Agent摘要频率可以调低比如每 10 轮触发一次因为编码场景的上下文更值钱。如果做客服摘要频率要高每 3 轮就压一次因为客服对话轮次多但单轮信息少。验证模型效果的时候可以到模型对话页面手动跑几轮对比开摘要和不开摘要的回答质量。接入文档里有完整的参数说明API Keys 页面管理你的密钥。长期跑编码任务的话Coding Plan 的额度模型更适合高频调用。代码里记得加日志记录每次摘要前后的 token 数和字段变化。跑一周后回头看日志你会清楚知道自己的压缩策略是不是合理。摘要不是一劳永逸的配置而是需要根据实际对话分布持续调的参数。
返回列表