资讯详情

Message 和对话

📅 2026/10/14 19:36:47 | 华诺云谱 👁 阅读
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年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑