逐日AI
第 1 周 · D6约 6 小时

消息、上下文工程与压缩、会话存储/恢复/分叉(dg M06/M08/M09/M10)

搞懂消息在 Agent 内部怎么组织和传递,上下文满了怎么压缩,以及会话如何持久化、恢复和分叉。

今日目标 0/3

登录后可以勾选并保存进度。

今日目标

  1. 能画出一条消息从用户输入到写入历史的完整流转路径
  2. 能实现一个简单的上下文压缩策略,在窗口快满时自动触发
  3. 能实现会话的持久化保存、恢复继续对话,以及从某一步分叉出新会话

昨天工具学会了自纠错,但 D5 结尾留了一笔账:每次工具调用往 messages 里塞两条消息,工具返回那条常是上千 token 的 JSON——工具越好用历史涨得越快,最后撑爆上下文窗口。今天正面解决它,读完回来勾掉三条目标。

小白版讲解

一条消息的一生:Agent 的记忆就是这个数组

小学时有种玩法叫接力日记:本子在班里传,轮到你就得从头翻一遍才知道该接着写什么。本子越厚翻得越久,但你没得选。Agent 的记忆就是这本本子——D1 说过模型无状态,"多轮对话"全靠你在客户端维护的那份 messages 数组。今天补它内部的结构:每条消息都有一个 role,D1 讲了三种(systemuserassistant),D2 加上工具后多了第四种——tool,装工具执行完的返回值。

于是一条消息的流转路径是:用户输入进来追加一条 user;发给模型;模型决定调工具,返回的 assistant 里带着 tool_calls,原样追加;执行工具,结果包成一条 tool 追加;数组重发一遍,模型这次才吐出给用户看的文字,这条 assistant 同样要追加。一轮带工具的对话,往历史里写的不是一条,是四条。

messages.js
// Agent 的"记忆"就是这个数组:每一轮都要把它整个重新发给模型
const history = [
  { role: 'system', content: '你是订单助手。' },
  { role: 'user', content: '我三天前下的单什么时候到?' },
]
 
// 一次工具调用往历史里塞两条:模型的调用请求 + 工具的返回结果
function appendToolRound(history, call, result) {
  history.push({ role: 'assistant', content: '', tool_calls: [call] })
  history.push({ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) })
}
 
// 粗估 token:按 D1 的换算中文约 1.4 个字符一个 token,这里干脆一个字符算一个,
// 故意往高了估——低估会让压缩阈值永远触发不了,那是这里唯一不能犯的错
const estimateTokens = (msgs) =>
  msgs.reduce((sum, m) => sum + (m.content ?? '').length + 4, 0)

代码里那个 estimateTokens 值得多说一句。D1 算过,"你好,世界"这五个字符大约切成 3 到 4 个 token,也就是一个汉字差不多就是一个 token。所以这里直接拿字符数当 token 数,比真实值略高——方向是故意选的:估低了,真实占用顶到 100% 你的数字才刚过 70%,压缩阈值永远触发不了,等发现时已经是模型端甩回来的硬错误。

拿它算一笔账,就知道 D5 那句警告有多实在。一次订单库查询返回一张表就两三千 token;假设每轮一次工具调用、结果按 800 token 算,加上一问一答的文字,一轮进账约 1000 token,聊 30 轮就是 3 万,而它在第 31 轮要被完整重发一次。真正撑爆历史的从来不是用户打的字,是工具回给模型看的东西——它恰恰是你越把 Agent 做好用就涨得越快的那部分。所以问题不是"会不会满",是"满了怎么办"。

上下文工程:在固定预算里决定放什么

D1 用办公桌讲过上下文窗口是什么,今天直接进下一个问题:桌子就这么大,堆满了怎么办。换个场景——出差前打包登机箱:容积是死的,衣服、电脑、资料都想带,打包的本质不是"塞得下多少",而是在固定容积里排优先级,还要给回程买的东西留空。上下文工程(context engineering)正是如此:你得决定每次请求放什么、放多少。

这个预算至少拆成四块,很多人只算了一块:

占用项谁在决定它的大小常见量级
系统提示词你自己写的那段(D4 的人设 + 能力边界 + 输出要求 + 动态上下文)300 到 800 token
工具定义你注册了多少个工具、每个的描述和参数 schema 有多长10 个工具约 1000 到 1500 token
对话历史聊了多少轮、工具返回了多少东西上不封顶
输出预留你希望模型这次最多说多少字通常 1000 到 4000 token

第二行最容易被漏算。工具定义每一轮都要重发,和历史一样按量计费,且会在你注册第 11 个工具那天无声变长。这是 D5 那条原则的另一面:写得越细模型用得越准,每个字却要每轮付一次钱。

上下文窗口是怎么被塞满的1/5
已用 20 / 100 token
system 人设20 tok
上下文窗口就是模型的桌面,大小固定。system 人设先摆上去,它通常要一直留着。

所以"窗口 128k"从来不等于"你有 128k 可用"。扣掉前三块,真正留给历史的往往只有六到七成,而且必须留出安全余量——撞上限时模型端返回硬错误,用户看到的是彻底失败,不是"少记得一点"。做法是给历史单独划一条预算线,触碰就动手。那么,动什么手?

上下文压缩:只留决议,不留逐字稿

开完两小时的会,没人把逐字稿发到群里,发的是一份纪要:谁负责什么、下周几交、哪几条没谈拢。逐字稿里跑题的那十分钟,散会那刻就该扔掉——后续要用的从来不是过程,是结论

上下文压缩就是给对话开纪要,要回答三问:何时触发、压掉什么、留什么。

什么时候触发。 用阈值,不用定时器,也不要等报错。常见做法是历史占用超过预算七成就触发。为什么不是九成?压缩本身要调一次模型写摘要,有延迟也可能失败;卡到九成才动手,摘要一超时或被限流,下一轮就撞墙。七成是留给你自己的抢救时间。

压缩掉什么。 优先压最早那段里"过程性"的内容:模型的中间推理、已被消费的工具原始返回值、用户后来推翻的需求。它们的共同点是价值已经沉淀进后面的结论里,原文再留只是重量。

保留什么。 这条最要紧,写错了模型会当场失忆:系统提示词永远原样保留(它不属于"历史");最近若干条保留原文,模型最依赖刚发生的事;用户声明过的约束和事实("我住上海""预算两千以内")必须写进摘要,不能当过程扔掉。还有一条最容易翻车——切口必须落在一轮的开头:切在 assistanttool_calls 和对应的 tool 结果之间,下一次请求就出现"有调用没结果"的悬空消息,绝大多数厂商的 API 直接返回 400。

compress.js
const MAX_TOKENS = 8000 // 给历史单独划的预算,不是模型窗口上限
const TRIGGER_RATIO = 0.7 // 用到七成就压,留出摘要调用失败的抢救时间
const KEEP_RECENT = 6 // 最近 6 条保留原文:模型最依赖刚刚发生的事
 
// 往前找到最近一个不是 tool 结果的位置,保证切口落在一轮的开头
function alignToTurnStart(msgs, index) {
  let i = index
  while (i > 0 && msgs[i].role === 'tool') i--
  return i
}
 
async function compressIfNeeded(history, summarize) {
  if (estimateTokens(history) < MAX_TOKENS * TRIGGER_RATIO) return history
 
  const system = history.filter((m) => m.role === 'system')
  const rest = history.filter((m) => m.role !== 'system')
  const cut = alignToTurnStart(rest, Math.max(0, rest.length - KEEP_RECENT))
  if (cut === 0) return history
 
  // 摘要本身就是一次模型调用:要花钱、要等,所以别卡到最后一刻才做
  const digest = await summarize(rest.slice(0, cut))
  return [...system, { role: 'user', content: `【早期对话摘要】${digest}` }, ...rest.slice(cut)]
}

效果算给你看。前面那个 30 轮、3 万 token 的例子(假设一路没压),压缩后剩"一条约 600 token 的摘要 + 最近 6 条原文"。6 条按本节自己的口径折算:一轮 4 条约 1000 token,6 条就是一轮半、1500 上下。合计约 2100,只有原来的十四分之一。而且这笔账是持续的:往后每轮都按压缩后的长度计费,省的不是一次,是所有剩余轮次。代价是一次额外的模型调用,外加一次不可逆的信息丢失。

会话持久化:小票还是月账单

超市小票一笔一笔往下印,印错了也不擦掉重印;银行月账单则是每月覆盖一份最新汇总。会话存盘就是这两种选择:**只追加(append-only)**是小票,每产生一条消息就往文件尾部追加一行,历史永不修改;**快照(snapshot)**是月账单,每轮结束把整个会话覆盖写一次。

优先选只追加,三条理由都很实在:写入不受历史长度影响;天然可回放到任意一步,这是下两节"恢复"和"分叉"的地基;出问题时你有完整审计轨迹。快照的价值是加速——历史几千条时全量重放太慢,常用组合是"只追加为准 + 定期快照"。

格式上一行一条 JSON 的 JSONL 最省事:可读、能 tail 直接看、追加不必解析整个文件。元信息(父会话是谁、从第几条分叉、已压缩到第几条)单独放一个小文件:

TextText
.sessions/
  s-8f3a.jsonl        # 一行一条消息,只追加不改写
  s-8f3a.meta.json    # 会话元信息:id、parentId、forkedAt、压缩水位
TextText
{"seq":1,"role":"user","content":"我三天前下的单什么时候到?","ts":"2026-09-04T10:00:00Z"}
{"seq":2,"role":"assistant","content":"","tool_calls":[{"id":"c1","name":"query_order"}],"ts":"2026-09-04T10:00:01Z"}
{"seq":3,"role":"tool","tool_call_id":"c1","content":"…订单 JSON…","ts":"2026-09-04T10:00:02Z"}

上生产时文件换成数据库(在 Postgres 里建 sessionsmessages 两张表),结构不变。

会话恢复:读档要读对存档点

打游戏读档,你期待"回到当时那一刻继续玩"。但存档只存了角色位置没存背包,读出来你就站在原地一身空——恢复的质量取决于当初存了什么。会话也一样,有两个坑几乎人人都踩。

第一个坑:把当时的系统提示词一起存进历史了。D4 立过规矩,它的结构最后一块是"动态上下文",里头有"现在时间"这种活的东西。三天前存的会话今天读出来,模型以为还是三天前,日期直接算错。正确做法:系统提示词不进持久化历史,每次恢复时用当前时间现拼一条,再接上磁盘里读出来的对话部分。

第二个坑更隐蔽:存档存在了工具调用中途。进程在模型返回 tool_calls 之后、工具结果写回之前崩了,磁盘上最后一条就是个悬空调用,下次恢复发出去就是上一节那个 400。所以加载完必须做完整性校验:若尾部是没有配对结果的 tool_calls,要么补一条"工具执行被中断,请重新调用"的 tool 消息让模型重来(正好复用 D5 的错误回传机制),要么丢弃这条不完整的尾巴。

第三件要一起恢复的是压缩水位:这个会话已压到第几条。不恢复它,阈值判断会把早已摘要过的那段又数一遍,于是要么反复压缩、要么迟迟不压。

会话分叉:从存档点开一条新线

游戏里最爽的玩法之一:关键选择前先存个档,选 A 玩一遍,不满意就读档回来选 B。两条线共享前面的剧情,之后各走各的,而且A 线怎么玩都不改变存档本身

会话分叉就是这件事,定义很干净:取历史前 k 条复制成一个新会话,记住父会话是谁、从第几条切的;父会话一个字都不动。 最后半句是它和"回滚"的分界线——回滚砍掉一截历史继续用,是破坏性的;分叉长出一条新枝,两边都能继续。

它在真实产品里对应三个高频场景:"重新生成"(用户不满意点重试,就是从倒数第二条分叉)、提示词 A/B 实验(同一段真实历史接上两版系统提示词各跑一遍,比造假数据可信得多)、线上问题复现(把出事那条会话分叉到自己名下,怎么折腾都不影响原会话)。

fork.js
import { randomUUID } from 'node:crypto'
 
// 分叉 = 复制前 k 条 + 记住从哪来。父会话只读,这是它和"回滚"的根本区别。
function forkSession(parent, atIndex) {
  const cut = alignToTurnStart(parent.messages, atIndex)
  return {
    id: randomUUID(),
    parentId: parent.id,
    forkedAt: cut,
    // 必须深拷贝:直接引用父会话的消息对象,两条分支迟早互相污染
    messages: structuredClone(parent.messages.slice(0, cut)),
    createdAt: new Date().toISOString(),
  }
}

四份代码里那两条注释值得对着看:JS 和 Python 必须显式深拷贝,因为切片只复制外层容器,里面还是同一批可变对象;Java 的 record 和 Swift 的 struct 元素不可变或是值类型,拷贝列表就够。这不是语法差异,是数据模型的差异——把消息设计成不可变的,分叉就从"记得深拷贝"变成"想错都难"。

工程代价有三笔。存储放大:每次分叉都全量复制前 k 条,分叉五次就是六份历史;规模上来后改成只存父引用和切点、读时拼接,代价是读路径复杂。成本归属:各分支各自烧钱,账要能顺着 parentId 聚合回一棵树,否则你看到的是一堆孤立会话。第三笔最容易被忽略:会话已经压缩过,而用户想从"已被摘要掉"的那段分叉,原文早没了——上一节那个 Callout 要求原始历史只追加不改写,就是为了这一刻。分叉能开到多深,取决于你有没有保留原文。

记忆分类:哪些跟着会话走,哪些跟着人走

导游手里有两样东西:一份是今天的行程单,写着九点集合、中午吃什么,团散了就是废纸;另一份是旅行社的客户档案,记着这位客人不吃辣、带着小孩,明年再来还用得上。

边界就在这儿。短期上下文是本次会话的 messages 数组,随会话结束作废、全量进请求——今天讲的压缩、恢复、分叉都在管它。长期记忆是跨会话的用户事实与偏好,不进 messages,而是存在外部系统里,用时按需检索、把捞到的几条注入请求。

判断一条信息该往哪放,问三句话就够:跨会话之后还需要吗?会随时间失效吗?能通过检索捞回来吗? "用户住上海"是"需要、不失效、能检索",进长期记忆;"刚才让我把第二段改成三句话"是"不需要、马上失效",留短期。介于两者之间的先留短期,反复出现再沉淀成长期偏好。

两者的工程差异是彻底的:短期上下文按 token 计费、受窗口约束、随会话删除;长期记忆按条存储、受检索质量约束,还要处理"用户改了主意,旧记忆怎么失效"。最常见的误用是把长期记忆当上下文一次性全塞进去——半年攒两百条偏好全塞进请求,既撑爆窗口,又用不相关的记忆干扰模型;正确姿势是只检索最相关的三五条。这套检索就是 RAG,第 12 天的正题。今天把边界画清楚就够:今天管"这次对话记得住",第 12 天管"这个人记得住"。

源码导读

动手实验

🧪 D6 实验:会话持久化 + 自动压缩

代码位置:labs/agent-30days/day-06-session-persistence

验收标准:

  1. MOCK=1 pnpm start 打印出压缩前后的对比,形如 [压缩] 33 条 / 3135 token → 10 条 / 916 token(降幅 71%)
  2. 连着跑两次 MOCK=1 pnpm start chat "…"(两个独立进程),第二次打印 [恢复] 从磁盘读到 N 条消息 且 N 大于 0。
  3. MOCK=1 pnpm start fork 4 分出新会话,用 --session= 在它上面追加一轮后,MOCK=1 pnpm start show main 显示父会话条数一条没变。
  4. 打开 .sessions/main.jsonl,能看到一行一条消息、只追加不改写,tool 那条也在。
  5. pnpm typecheck 通过,没有 any

starter/ 挖了四个练习点,MOCK=1 下完全离线跑通:模拟层会造出带工具调用的多轮对话让历史迅速涨到阈值,不用真花钱聊三十轮也能看到压缩触发。四个挖空处的默认值都会让程序"跑得通但明显不对"——token 恒为 0、历史压不短、重启读到 0 条、分叉污染父会话——先原样跑一次看清这四行报警。

  1. 先原样跑 MOCK=1 pnpm start,记下四行「练习未完成」提示,那就是待办清单。
  2. 实现 appendMessage 落盘:把每条消息按 JSONL 追加进 .sessions/main.jsonl,跑完 cat 一下,确认一行一条、tool 那条也在。
  3. 实现 estimateTokenscompressIfNeeded:超过预算七成时把早期消息摘要成一条,切口用给好的 alignToTurnStart 对齐,跑完能看到「降幅 71%」这样的对比行。
  4. MOCK=1 pnpm start chat "我住上海,帮我记一下",再另起一个进程跑 MOCK=1 pnpm start chat "刚才我说我住哪?",第二次要打印「从磁盘读到 N 条消息」并答得上来。
  5. 实现 forkSession 的深拷贝,用 fork 4 分出新会话、用 --session= 在它上面追加一轮,再 show main 确认父会话没被改动。

面试题

今天 4 道题在下方题库区,侧重上下文管理、长对话和记忆分类。先看"分析过程"再看要点——第 1 题的追问(摘要该用哪个模型、失败了怎么办)最容易被追着问。

检查清单与明日预告

  • 能画出一条消息从用户输入到写入历史的完整流转路径
  • 能实现一个简单的上下文压缩策略,在窗口快满时自动触发
  • 能实现会话的持久化保存、恢复继续对话,以及从某一步分叉出新会话
  • 能说出上下文预算的四块构成,并说清压缩切口为什么必须对齐到一轮开头
  • 实验的 5 条验收标准全部通过
  • 4 道面试题不看要点也能答出至少 3 道

明天(D7)我们把前六天的东西合成一个真正的服务:用 Fastify 暴露 SSE 接口,用 Docker 打包,再做 W1 复盘。顺序是故意的——今天会话存在本地文件里,进程重启还能读回来,但服务化之后这条路立刻走不通:多个实例各写各的磁盘,用户第二次请求被负载均衡打到另一台,历史就凭空消失。会话状态必须挪出进程、挪出单机,这是从脚本变成服务绕不过去的第一道坎。

面试题库

  • 长对话里上下文放不下了,你会怎么压缩?什么时候触发、压掉什么、保留什么?When a long conversation outgrows the context window, how do you compress it — when do you trigger, what do you drop, and what do you keep?
    国内高频海外高频进阶#context-engineering#compression#cost

    分析过程 · 先想清楚再作答

    1. 这题的区分度不在「用摘要」三个字上,几乎人人都答得出。区分度在你有没有说出触发时机和保留清单——只答「让模型总结一下前面的对话」的,面试官会判定你没在长对话上线过。
    2. 先把问题拆成三问再逐个答:什么时候压、压掉什么、保留什么。这个拆法本身就是加分项,因为它说明你把压缩当成一个策略而不是一个函数。
    3. 触发用阈值不用定时器,也不能等报错。给一个具体数字并解释它:历史占用到预算的七成就动手,因为摘要本身是一次模型调用,有延迟也可能失败,卡到九成再压,一旦摘要超时下一轮就直接撞窗口上限了——七成是留给自己的抢救时间。
    4. 压掉的是过程性内容:中间推理、已经被消费完的工具原始返回值、用户后来推翻的需求。它们的共同点是价值已经沉淀进后面的结论里。保留的是系统提示词(它不属于历史)、最近若干条原文、以及用户明确声明过的约束和事实——后者写错了模型会当场失忆。
    5. 再补一条别人不会说的:切口必须对齐到一轮的开头。切在 assistant 的 tool_calls 和对应的 tool 结果中间,下一次请求就有了悬空调用,多数厂商的 API 直接返回 400。这一条最能证明你真的调过。
    6. 可以预期的追问有两个。一是摘要该用哪个模型:用便宜的小模型就行,摘要是抽取任务不是推理任务,这也接上了 D4 的分层路由。二是摘要调用失败了怎么办:降级到不摘要的滑动窗口(直接丢最早的几轮),保证请求发得出去,别让压缩失败连带整轮对话失败。

    How to reason about it · think before answering

    1. Saying 'summarize it' earns nothing — everyone says that. The signal is whether you name a trigger point and a keep-list; without those you sound like someone who never ran a long conversation in production.
    2. Split it into three questions before answering: when to compress, what to drop, what to keep. The split itself scores, because it frames compression as a policy rather than a function.
    3. Trigger on a threshold, not a timer, and never on an error. Give a number and justify it: compress at roughly 70% of the history budget, because summarizing is itself a model call that can be slow or fail. Waiting until 90% means one timed-out summary call and the next turn slams into the window limit.
    4. Drop the process: intermediate reasoning, raw tool payloads already consumed, requirements the user later reversed — their value has already settled into later conclusions. Keep the system prompt (it is not history), the most recent turns verbatim, and any constraint or fact the user stated explicitly. Getting that last one wrong makes the model visibly forget.
    5. Add the detail others miss: the cut must land on a turn boundary. Slicing between an assistant tool_calls message and its matching tool result leaves a dangling call, and most providers reject that request with a 400. This is the line that proves hands-on experience.
    6. Two follow-ups to expect. Which model summarizes? A cheap small one — summarization is extraction, not reasoning, which ties back to tiered routing. And what if the summary call fails? Degrade to a plain sliding window that drops the oldest turns, so a failed compression never fails the whole turn.

    答题要点

    • 阈值触发:历史占用到预算七成就压,因为摘要本身是一次会失败、有延迟的模型调用,必须留抢救余量
    • 压过程、留结论:丢中间推理和已消费的工具原始返回,保留系统提示词、最近若干条原文、用户明确声明的约束与事实
    • 切口必须对齐到一轮开头,切在 tool_calls 与 tool 结果之间会让下一次请求返回 400
    • 压缩是有损且不可逆的:原始历史另存一份只追加,发给模型的是压缩版,需要回溯或分叉时读原始版
    • 摘要用便宜的小模型;摘要失败要能降级成滑动窗口,别让压缩失败连累整轮对话

    Key points

    • Threshold-triggered at about 70% of the history budget, because the summary call itself is a slow, fallible model call that needs headroom
    • Drop process, keep conclusions: discard intermediate reasoning and consumed raw tool payloads; keep the system prompt, the recent turns verbatim, and explicit user constraints and facts
    • Align the cut to a turn boundary — slicing between tool_calls and its tool result makes the next request fail with a 400
    • Compression is lossy and irreversible: keep an append-only original, send the compressed version, and read the original when you need to backtrack or fork
    • Summarize with a cheap small model, and degrade to a sliding window if the summary call fails so compression failure never fails the turn
  • 会话的持久化、恢复和分叉分别解决什么问题?实现时各有什么坑?What problems do session persistence, restore, and forking each solve, and what goes wrong in each?
    国内高频海外高频进阶#session-management#persistence#forking

    分析过程 · 先想清楚再作答

    1. 题干把三件事并列,考的其实是你能不能分清它们各自的动机——很多人会把三个都答成「存下来」,那就丢掉了全部区分度。
    2. 先一句话各给一个动机:持久化解决「进程重启和跨机器请求」,恢复解决「加载回来还能接着聊」,分叉解决「同一段历史要走出两条不同的后续」。动机不同,所以数据结构的要求也不同。
    3. 持久化的关键选择是只追加还是快照。答只追加并给理由:写入不受历史长度影响、能回放到任意一步、有审计轨迹;快照只是读加速手段,工程上常见的是「只追加为准 + 定期快照」。这一条直接决定了分叉能不能做。
    4. 恢复的两个坑要主动说。一是把当时的系统提示词一起存进了历史,里面有「现在时间」这类动态上下文,三天后读出来模型的日期判断全错——系统提示词不进持久化历史,每次现拼。二是存档存在了工具调用中途,最后一条是没有配对结果的 tool_calls,直接发出去就是 400,加载后必须做完整性校验,补一条「执行被中断」的结果或丢弃这条尾巴。
    5. 分叉最容易被忽视的是「父会话只读」这条语义。分叉不是回滚:回滚砍掉历史继续用,是破坏性的;分叉复制前 k 条长出新枝,两边都能继续。实现上要深拷贝,直接引用父会话的消息对象会让两条分支互相污染。
    6. 可以预期的追问:分叉多了存储怎么办?答按父引用加偏移存、读时拼接,代价是读路径变复杂;再顺手补一句成本要能顺着 parentId 聚合成一棵树,否则账算不清是哪个用户的哪次重试花的钱。

    How to reason about it · think before answering

    1. The question lists three things side by side, so it is really testing whether you can separate their motivations. Answering 'they all save the conversation' throws away the entire signal.
    2. Give one motivation each in a sentence: persistence survives process restarts and multi-instance routing, restore lets a loaded history keep the conversation going, forking lets one history grow two different futures. Different motivations imply different data structures.
    3. The key persistence choice is append-only versus snapshot. Choose append-only and justify it: writes are independent of history length, any point can be replayed, and you keep an audit trail. Snapshots are a read optimization, so production usually means append-only as the source of truth plus periodic snapshots. This choice is what makes forking possible at all.
    4. Volunteer the two restore traps. First, persisting the system prompt inside the history: it carries dynamic context like the current time, so a session loaded three days later has the model reasoning from a stale date. Rebuild the system prompt fresh on every load. Second, a session saved mid tool call ends with an unmatched tool_calls message; replaying it verbatim gets a 400, so validate on load and either append an 'execution interrupted' tool result or drop the dangling tail.
    5. For forking, the overlooked point is that the parent stays read-only. Forking is not rollback: rollback truncates and mutates, forking copies the first k messages into a new branch and both sides continue. Deep-copy the messages — sharing the parent's objects lets the branches contaminate each other.
    6. Expect the follow-up on storage: reference the parent plus an offset and stitch on read, at the cost of a more complex read path. Add that cost must aggregate up the parentId tree, or you cannot tell which user's retry burned which tokens.

    答题要点

    • 持久化解决进程重启与跨实例,选只追加:写入不受历史长度影响、可回放任意一步、有审计轨迹;快照只是读加速
    • 恢复要现拼系统提示词,不能把带「现在时间」的那份存进历史,否则读出来日期判断全错
    • 恢复必须做完整性校验:尾部悬空的 tool_calls 要补一条中断结果或丢弃,否则下一次请求返回 400;压缩水位也要一起恢复
    • 分叉是复制前 k 条并记住父会话与切点,父会话只读——这是它和破坏性回滚的根本区别,实现上必须深拷贝
    • 分叉的代价是存储放大与成本归属,规模上来后改成存父引用加偏移,账要能顺着 parentId 聚合成树

    Key points

    • Persistence handles restarts and multiple instances; prefer append-only for constant-cost writes, replayability and an audit trail, with snapshots purely as a read optimization
    • On restore, rebuild the system prompt fresh — persisting the one containing the current time makes the model reason from a stale date
    • Validate on restore: a dangling tool_calls tail needs an 'interrupted' tool result or must be dropped, or the next request returns 400; restore the compression watermark too
    • A fork copies the first k messages and records parent and cut point, leaving the parent read-only — that is what separates it from destructive rollback, and it requires a deep copy
    • Forking costs storage amplification and muddled cost attribution; at scale store a parent reference plus offset and aggregate spend up the parentId tree
  • 短期上下文和长期记忆的边界怎么划?一条信息该往哪放,你的判断依据是什么?Where do you draw the line between short-term context and long-term memory, and how do you decide where a given fact belongs?
    国内高频海外高频基础#memory#context-engineering

    分析过程 · 先想清楚再作答

    1. 这题看着像概念题,其实考的是你有没有一条可执行的判据。背出「短期在 messages 里、长期在向量库里」只是描述现状,答不出「为什么这条该进长期」就没有区分度。
    2. 先把两者的工程属性摆出来,边界自然就清楚了:短期上下文随会话结束作废、全量进请求、按 token 计费、受窗口约束;长期记忆跨会话存在、不进请求而是检索后注入、按条存储、受检索质量约束。
    3. 给一条可复用的判据,这是本题的核心:问三句话——跨会话之后还需要吗、会随时间失效吗、能通过检索捞回来吗。三个都是「是」就进长期记忆,第一个是「否」就留在短期。举例说明:用户住上海进长期,用户刚才让我把段落改成三句话留短期。
    4. 点出最常见的误用:把长期记忆当上下文一次性全塞进去。用了半年攒两百条偏好,全塞进请求既撑爆窗口,又因为大量不相关记忆干扰模型判断——长期记忆的价值在于按需检索出最相关的三五条,不在于存了多少。
    5. 可以预期的追问:长期记忆怎么更新和失效?答要点是记忆要带时间戳和来源,用户改了主意要能覆盖旧记忆而不是并存两条矛盾的;再补一句删除权——用户要求删数据时,长期记忆是必须能定位并整体删掉的那一部分。

    How to reason about it · think before answering

    1. This looks conceptual but is really asking for an operational test. Reciting 'short-term lives in messages, long-term lives in a vector store' just describes the status quo and gives no signal.
    2. Lay out the engineering properties and the boundary draws itself: short-term context dies with the session, ships in full on every request, is billed per token and capped by the window; long-term memory spans sessions, is retrieved and injected rather than always sent, is stored per item and capped by retrieval quality.
    3. Give a reusable test — this is the core of the answer. Ask three questions: is it still needed after this session ends, does it expire with time, can retrieval find it again? Three yeses means long-term; a no on the first means it stays short-term. Illustrate: 'the user lives in Shanghai' is long-term, 'the user just asked me to shorten that paragraph to three sentences' is not.
    4. Name the common failure: stuffing all long-term memory into the prompt. Two hundred preferences accumulated over six months will both blow the window and drown the model in irrelevance. The value of long-term memory is retrieving the three or four relevant items, not the volume stored.
    5. Expect the follow-up on updates and expiry: memories need timestamps and provenance, and a changed preference must overwrite rather than coexist with a contradictory one. Add the deletion angle — long-term memory is the part you must be able to locate and erase when a user asks for their data to be deleted.

    答题要点

    • 短期上下文随会话作废、全量进请求、按 token 计费受窗口约束;长期记忆跨会话、按需检索后注入、按条存储受检索质量约束
    • 判据三问:跨会话还需要吗、会随时间失效吗、能被检索捞回来吗——三个都是就进长期记忆
    • 常见误用是把长期记忆整包塞进上下文,既撑爆窗口又用不相关的记忆干扰模型,正确做法是检索最相关的三五条
    • 长期记忆要带时间戳和来源,用户改主意时覆盖旧记忆,避免两条矛盾记忆并存
    • 长期记忆是合规上必须能按用户定位并整体删除的那一部分,短期上下文随会话删除即可

    Key points

    • Short-term context dies with the session, ships in full, and is billed per token under the window cap; long-term memory spans sessions, is retrieved on demand, and is capped by retrieval quality
    • The three-question test: is it needed after this session, does it expire, can retrieval find it — three yeses means long-term
    • The common failure is injecting the whole memory store, which blows the window and drowns the model in irrelevance; retrieve the three or four relevant items instead
    • Long-term memories need timestamps and provenance so a changed preference overwrites the old one instead of contradicting it
    • Long-term memory is the part that must be locatable and deletable per user for compliance, while short-term context simply dies with the session
  • 上下文工程和 RAG 检索是什么关系?只做 RAG 不做上下文管理会出什么问题?How do context engineering and RAG relate, and what breaks if you do RAG without context management?
    国内高频海外高频深入#context-engineering#rag#retrieval

    分析过程 · 先想清楚再作答

    1. 题眼在「关系」。把两者说成并列的两种技术是最常见的失分答法——正确的框架是包含关系:上下文工程是「决定这次请求里放什么」,RAG 是它的一种供给手段,负责「从外部捞该放进去的东西」。
    2. 拆开看职责就清楚了:RAG 解决的是「信息不在模型参数里、也不在当前对话里」,靠检索把它取回来;上下文工程解决的是「取回来的东西、加上历史、加上工具定义,一共放不放得下、该按什么优先级放」。前者管来源,后者管预算。
    3. 所以只做 RAG 不做上下文管理会出三类问题,最好按这个顺序说。第一是挤占:检索回来的文档动辄几千 token,直接拼进去把对话历史挤没了,模型记得住资料却忘了用户三句话前说过什么。第二是干扰:召回条数调大看着安全,实际上不相关的片段会稀释模型注意力,准确率不升反降。第三是成本:检索结果每一轮都重发,一段两千 token 的资料聊十轮就付了十次。
    4. 给出正确的组合姿势:先给历史和检索结果各划一条预算线,检索结果只保留 top-k 且不跨轮重复注入,历史超线就压缩,两条线加起来必须留出输出空间。这套「分账」的说法比笼统的「要平衡」有说服力得多。
    5. 可以预期的追问:检索结果该放在系统提示词里还是当成一条 user 消息?答放在靠近当前问题的位置通常效果更好,而且要标注来源便于模型区分「资料」和「用户说的话」;顺带说清它是一次性上下文,不该被写进长期会话历史里反复重发。

    How to reason about it · think before answering

    1. The hinge word is 'relate'. Treating them as two parallel techniques is the standard weak answer — the right frame is containment: context engineering decides what goes into this request, and RAG is one supply mechanism that fetches what should go in.
    2. Separate the responsibilities and it becomes obvious: RAG solves 'the information is neither in the weights nor in this conversation' by retrieving it; context engineering solves 'the retrieved chunks plus the history plus the tool definitions all have to fit, in some priority order'. One owns sourcing, the other owns budget.
    3. So RAG without context management breaks in three ways, best delivered in this order. Crowding: retrieved documents run to thousands of tokens and squeeze out the conversation, so the model knows the manual but forgot what the user said three turns ago. Interference: raising top-k feels safe but irrelevant chunks dilute attention and accuracy drops instead of rising. Cost: retrieved text is resent every turn, so a 2k-token passage costs ten times over ten turns.
    4. Give the correct combination: budget history and retrieval separately, keep retrieval to top-k without re-injecting the same chunks every turn, compress history when it crosses its line, and make sure both lines together still leave room for output. This 'separate budgets' framing lands much better than a vague 'you need to balance them'.
    5. Expect the follow-up on placement: putting retrieved context near the current question usually works better, and it should be labeled with its source so the model can tell reference material from what the user actually said. Note too that it is single-turn context and should not be written into the persisted history and resent forever.

    答题要点

    • 不是并列关系而是包含关系:上下文工程决定这次请求放什么,RAG 是给它供货的一种手段,负责把不在模型和对话里的信息检索回来
    • 只做 RAG 会挤占历史:几千 token 的检索结果把对话挤没,模型记得住资料却忘了用户刚说的话
    • 召回条数越大越准是错觉:不相关片段会稀释注意力,准确率反而下降,应控制 top-k
    • 检索结果每轮重发会持续计费,属于一次性上下文,不该写进持久化历史反复重发
    • 正确姿势是给历史和检索各划一条预算线,历史超线就压缩,两条线之外还要留出输出空间

    Key points

    • They are not parallel: context engineering decides what enters the request, and RAG is one supply mechanism for information that is neither in the weights nor in the conversation
    • RAG alone crowds out history — multi-thousand-token retrievals evict the conversation, so the model knows the docs but forgot the user's last request
    • A bigger top-k is not safer: irrelevant chunks dilute attention and accuracy drops, so cap retrieval
    • Retrieved text is single-turn context; persisting it into the history means paying for it on every subsequent turn
    • Budget history and retrieval on separate lines, compress history when it crosses its line, and leave room for the output on top of both

评论

登录后即可参与讨论

还没有评论,来说第一句。