Message 和对话
专栏AI 全栈开发18上一篇 17《LLM 请求完整参数》仍然站在“一次请求”的视角model、messages、temperature、输出预算都只描述这一轮。真正的聊天产品还缺一层——会话状态。Message 是一条事实Conversation 是一个产品资源而 Context 是每一轮根据预算临时组装出来的视图。把这三件事分开后面才谈得上记忆、隔离和成本。一、从“一次请求”走到“一段对话”用户先说“帮我总结这份合同”下一轮只追问“那赔偿上限呢”。第二句话离开上一轮就没有完整语义所以产品不能只保存最后一个 prompt。每次请求都要在一个会话里读到“必要的前文”再把新回复写回去。但这里最容易走向另一个极端把数据库里从第一句到现在的所有历史每一轮原样全发给模型。它前几轮能跑几十轮后就会越来越慢、越来越贵最后撞上上下文窗口。图 1 完整历史、会话资源和本轮模型上下文是三层不同的数据结构二、先把 Message、Conversation、Context 分开概念它保存什么生命周期Message一条用户 / assistant / tool 事实记录写入后通常只追加不因裁剪而消失Conversationcid、owner、状态以及一组有序 Message从创建到活跃、归档、删除Context这一轮真正发给模型的消息视图每次请求都重新组装可以不同最重要的一条原则存储保存“事实”Context 只表达“这一轮模型应该看到什么”。所以“清空上下文”不等于“删除聊天记录”同样Provider 是否替你保存了一部分会话状态也不替代产品自己的权限、审计和历史管理。三、Message 不应该只有 role content最小 Demo 里一条消息只有 role 和 content 就够了进入产品后至少还需要服务端顺序和与工具调用相关的元数据。顺序尤其不能依赖客户端时间戳因为多设备和网络重试都会让到达顺序与本地时间发生冲突。字段作用seq服务端分配的单调顺序决定语义先后roleuser / assistant / tool 等角色content文本或工具结果的可见内容tool_call_id / tool_calls把 assistant 的调用意图与 tool 结果关联起来created_at审计、排序辅助不作为唯一顺序真相四、最小实现先做一个内存 Conversation最小建模Message 是事实Conversation 是有序容器from dataclasses import dataclass from datetime import datetime, timezone dataclass(frozenTrue) class Message: seq: int role: str content: str created_at: str class Conversation: def __init__(self, cid, owner): self.cid cid self.owner owner self._messages [] def append(self, role, content): msg Message( seqlen(self._messages) 1, rolerole, contentcontent, created_atdatetime.now(timezone.utc).isoformat(), ) self._messages.append(msg) return msg def history(self): return list(self._messages)这里的 len1 只用于单进程演示。生产环境要让数据库序列、事务或每会话原子计数器分配 seq否则两个请求同时 append 时会拿到相同序号。五、历史记录和模型上下文绝不能是同一份东西UI 里的聊天记录要尽可能完整因为用户要翻页、导出、审计模型上下文却必须受预算约束。一次“组装 Context”的工作本质上就是从完整历史里选出一个结构合法、语义尽量完整、又装得进窗口的子集。图 2 上下文裁剪只改变“本轮视图”不应该物理删除存储里的历史消息六、裁剪按 token不按消息条数“只保留最近 20 条”看起来简单但一条短回复可能只有几个 token一段粘贴的代码可能是几千 token。真正的限制来自 token 窗口所以预算单位也必须跟 token 对齐。• system / policy 先预留预算再给历史分配空间。• 优先保留最近轮次越早的内容越适合摘要或丢弃。• 给模型输出预留空间不要把整个窗口全部塞成输入。• 示例可以用粗略计数生产环境应使用对应模型/Provider 的 tokenizer 或可靠估算。七、tool_calls 要成块裁剪不能从中间切断一旦会话里出现 Agent 工具调用Context 就不再是“随便删几条消息”这么简单。assistant 发出的 tool_calls 与对应的 tool 结果在语义上属于同一个交互块如果只保留一半Provider 可能无法关联 tool_call_id模型也会看到一段残缺历史。因此裁剪的最小单位应该是“语义块”而不是单条数据库记录普通 user/assistant 轮次成对处理tool call tool result 作为原子块一起留或一起删。八、多会话隔离cid 只是定位符不是权限一个用户可以同时有多个会话不同用户也会访问同一套 API。cid 使用高熵值可以降低被枚举的风险但它绝不是授权机制。每一次 history、append、archive、delete 都必须从登录态拿当前用户再校验 Conversation.owner。• 不要信任前端自己传的 user_id 作为授权依据。• 不要因为 cid 是 UUID 就省略 owner 校验。• “会话不存在”和“无权访问”的外部返回语义要按你的安全策略统一设计避免泄露资源存在性。九、并发写顺序由服务端决定但不要把数据库事务挂在 LLM 上同一会话可能被两个浏览器标签页、手机和网页同时写。此时需要并发控制但不建议为了“原子性”把数据库事务或全局锁一直持有到 LLM 返回——一次模型调用可能持续数秒长事务会拖垮并发。策略怎么做适合场景每会话串行队列同一个 cid 的消息按队列顺序处理简单、强一致适合聊天主链路乐观版本读取 snapshot_version回写前检查版本是否仍一致允许并发但要处理冲突/重试事件追加所有 Message append-only按 seq 重建状态审计友好适合更复杂架构十、一个小型 ContextAssemblerContextAssembler按 token 预算保留完整语义块def assemble_context(system, blocks, token_budget, count_tokens): blocks: 已经按语义分组的历史块每块可能包含 1~N 条消息 count_tokens(messages): 使用当前模型的计数器 fixed [{role: system, content: system}] if system else [] kept [] # 从最近的语义块向前装block 内绝不拆开 for block in reversed(blocks): candidate fixed [m for b in reversed(kept) for m in b] block if count_tokens(candidate) token_budget: break kept.append(block) history [m for block in reversed(kept) for m in block] return fixed history这里把“怎么把原始 Message 分组成 block”留给适配层普通对话可以按一轮 userassistant 分组工具调用则把 assistant.tool_calls 和所有对应 tool result 放进同一个块。这样预算裁剪不会破坏结构。十一、ConversationService 只做会话层的事图 3 ConversationService 负责身份、顺序和状态Store、Assembler、LLM Client 各自独立把职责拆开后一轮 chat 的流程会非常清楚Service 校验 owner → 追加 user Message → 读取一个稳定 snapshot → Assembler 生成 Context → LLM Client 调模型 → 追加 assistant Message。下一篇的 Streaming 只会改变“回复如何返回”不会推翻这套会话模型。十二、清空上下文、归档、删除是三件事操作用户看到什么底层应该做什么清空上下文继续保留聊天记录但模型从新的起点开始写 context boundary / 新分支标记不必删历史归档会话不再活跃但还能查看修改状态停止继续写入或放到历史列表删除用户要求移除会话按产品与合规规则执行删除/保留审计十三、几个最容易把对话做坏的地方• 误区 1每轮把全量历史直接发给模型。越聊越贵最终撞上下文窗口。• 误区 2按消息条数裁剪。真正限制是 token而不是“第 20 条”。• 误区 3裁剪时拆断 tool_calls 与 tool result。上下文结构会变成残缺状态。• 误区 4把数据库历史和模型 Context 当成同一个对象。清空上下文时就会误删用户记录。• 误区 5UUID 当权限控制。cid 只能定位资源owner 校验才是授权。• 误区 6持有数据库事务等待 LLM。外部调用一慢整个会话服务的并发都会被拖住。• 误区 7用客户端时间戳决定消息顺序。顺序必须由服务端 seq / 版本机制裁决。十四、下一篇下一篇 19《Streaming Model Output》会在这套会话模型之上打通 Model Provider → Backend → SSE → Browser 的流式链路模型还没生成完浏览器就开始显示同时处理断开、取消、增量落库以及“流式输出结束后如何写回 Conversation”。

相关新闻

总结 10.13

总结 10.13

今天学了数学的线性代数。了解了伴随矩阵直接用矩阵相等,因为可以得到aij的关系。然后是特征值和特征向量,了解了相似对角化,对称矩阵的相似对角化,不同特征值特征向量正交。不同特征值特征向量线性无关。然后是可对角化的条件&am…

2026/10/14 19:36:48 阅读更多 →
Redis容器化部署避坑指南:内存、网络与持久化三大核心问题解析

Redis容器化部署避坑指南:内存、网络与持久化三大核心问题解析

1. 为什么 Redis Docker 是现在最值得投入时间掌握的组合Redis 不是“又一个数据库”,它是现代应用架构里那根看不见却绷得最紧的弦。缓存击穿时用户刷不出首页,会话丢失导致用户反复登录,消息队列积压让订单状态卡在“处理中”——这些线上…

2026/10/14 19:36:48 阅读更多 →
实时交互模型火了:全双工如何让AI边听边说

实时交互模型火了:全双工如何让AI边听边说

实时交互模型火了:全双工如何让AI边听边说 一、概念澄清:实时交互模型不是又一个语音助手二、三代演进:从级联流水线到全双工三、全双工怎么工作:以 Thinking Machines 为例四、OpenAI 的两条腿:Realtime API 与原生语…

2026/10/14 19:35:48 阅读更多 →

最新新闻

权威测评!2026年最火AI论文工具榜单,免费款也能高效产初稿

权威测评!2026年最火AI论文工具榜单,免费款也能高效产初稿

一、实测方法论:5 大维度量化评估 本次实测覆盖全学科论文场景,重点考察以下维度,结果客观可复现: 表格 评估维度权重测评标准中文适配度25%是否符合 GB/T7714 引用规范,语言逻辑自然度,术语准确性免费功能…

2026/10/14 20:21:39 阅读更多 →
Linux嵌入式SD/MMC驱动调试:信号流向、三层架构与实战排错

Linux嵌入式SD/MMC驱动调试:信号流向、三层架构与实战排错

1. 项目概述:为什么一个SD/MMC驱动分析值得花时间深挖?在嵌入式Linux开发一线干了十多年,我经手过的板子从ARM9、ARM11到Cortex-A7/A53/A72,再到如今的RISC-V SoC,几乎每一块主控芯片的BSP包里,SD/MMC子系统…

2026/10/14 20:21:39 阅读更多 →
最小二乘法全攻略:从线性回归原理到代码实现与避坑实践

最小二乘法全攻略:从线性回归原理到代码实现与避坑实践

我第一次真正用上最小二乘法,是在处理一组传感器校准实验数据的时候。当时手里是一堆带噪声的采样点,教科书里的公式能帮我“找一条最合理的直线”,但工程上真正让人头疼的,其实是后面这些事:数据里混了离群点怎么办&a…

2026/10/14 20:21:39 阅读更多 →
大模型应用落地指南:从模型选型到Agent与RAG实践

大模型应用落地指南:从模型选型到Agent与RAG实践

1. 模型维度:基座能力的格局与选型逻辑距离大模型真正进入大众视野到现在,已经过了好几个年头。站在2026年10月这个时间窗口往回看,最明显的变化是:市面上能叫得出名字的大模型,已经从早期的"百家争鸣"收敛成…

2026/10/14 20:21:39 阅读更多 →
Linux MMC驱动深度解析:SD卡识别流程与SDIO协议栈实战

Linux MMC驱动深度解析:SD卡识别流程与SDIO协议栈实战

1. 项目概述:这不是教科书里的驱动理论,而是现场拆解一张SD卡如何被Linux真正“认出来” 你有没有遇到过这样的情况:插上一张SD卡,系统日志里刷出一串 mmc0: new high speed SDHC card at address 0001 ,但紧接着 d…

2026/10/14 20:21:39 阅读更多 →
从Zernike波前到PSF/MTF:光学像质仿真完整指南

从Zernike波前到PSF/MTF:光学像质仿真完整指南

做光学系统设计的人应该都有过这种体验:镜头草图画完、光线追迹也跑通了,但别人问"那成像到底清不清楚、能分辨多小的细节",你一时却拿不出一个直观的答复。光线追迹给的是光路,不是像质。真正用来回答"成像多锐利…

2026/10/14 20:20:38 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →