会话管理
Agent 运行在按需拉起、随时会被回收、且同一对话的多次请求可能落在不同实例上的环境里。如果只把数据放在进程内存,会遇到三个绕不开的问题:前端一刷新聊天记录就没了;换了个实例,模型接不上上一轮、表现为「失忆」;审批流、人工介入这类要「等一下再回来」的场景,实例回收后状态就丢了。
context.store 是 Makers Agent 内置的会话管理能力,零配置即可保存与读取对话历史,并为 Claude / OpenAI / LangGraph / DeepAgents Agent 框架提供原生适配。它的核心特性是跨实例持久化:数据落到平台 Blob 存储,不绑定进程——本次请求写入的数据,在当前请求结束、写入实例被回收、甚至换到另一个实例之后,只要带同一个
conversation_id,新请求仍然读得到。你不用自己管理任何外部数据库或缓存。说明:
跨实例持久化自 EdgeOne CLI 1.6.26 起支持,请将 CLI 升级到该版本或更高(
npm i -g edgeone@latest)。底层基于平台的 Blob 存储,Node 与 Python 两端 API 完全镜像(JavaScript
camelCase、Python snake_case),框架接入章节以 TypeScript 为主,每节附 Python 等价示例。在两种 Runtime 中使用
store 在 agents/ 与 cloud-functions/ 两类目录的代码中指向同一份数据。目录 | 使用入口 | 典型用途 |
agents/ | context.store | LLM 对话主路径,追加消息、Checkpointer |
cloud-functions/ | context.agent.store | 对话列表 API、消息查询 |
共通前提:对话身份
所有跨实例持久化都以
conversation_id 为 key。你不用自己解析:平台从请求头 makers-conversation-id 解析后注入 context.conversation_id,并在响应头回显 makers-conversation-id 和 makers-run-id。直接取 context.conversation_id 用即可,不要自己从请求体里另拼一个。对话 ID 作为存储 key 时长度上限 256 字符。框架接入
context.store 同时提供框架原生适配器和通用 API。framework | 推荐用法 | 入口(TS) |
claude-sdk | 框架原生 SessionStore | context.store.claudeSessionStore() |
openai-sdk | 框架原生 Session | context.store.openaiSession(sessionId) |
langgraph | 框架原生 Checkpointer + BaseStore | context.store.langgraphCheckpointer / .langgraphStore |
deepagents | 复用 LangGraph Checkpointer + BaseStore | context.store.langgraphCheckpointer / .langgraphStore |
crewai | 暂不支持,使用通用 API 自管 | - |
Claude Agent SDK
claudeSessionStore() 返回 SessionStore 协议实现(append / load / listSessions / delete / listSubkeys)。// typescriptconst sessionStore = context.store.claudeSessionStore()// pythonsession_store = context.store.claude_session_store()
Claude SDK 的
sessionId 必须是合法 UUID。如果你的 conversation_id 不保证是 UUID(比如 chat-2026-08-12-abc),用 claudeSessionBinding 把它映射成一个稳定的 UUID:本身是 UUID 就原样用,不是就生成一个并持久化映射,同一个 conversation_id 每次都换回同一个 sessionId。TS 示例:
// agents/chat/index.tsconst conversationId = context.conversation_id ?? ''const sessionStore = context.store.claudeSessionStore()// 把任意业务 ID 映射成稳定的 UUID sessionId(是 UUID 就原样返回,否则生成并持久化)const sessionId = await context.store.claudeSessionBinding(conversationId)// 判断这个 sessionId 之前有没有落过盘:有就 resume 续上,没有就当新会话// dir 不能省——SessionStore 的 blob key 按项目目录派生,丢了 dir 永远读不回const info = await getSessionInfo(sessionId, { dir: process.cwd(), sessionStore })const options: Record<string, any> = { model, systemPrompt, sessionStore }if (info) {options.resume = sessionId // 恢复已有会话} else {options.sessionId = sessionId // 新建会话}const q = query({ prompt: message, options })
Python 示例:
session_store = context.store.claude_session_store()session_id = await context.store.claude_session_binding(conversation_id)# 同样:getSessionInfo 判断 resume/new,options 里带上 session_store 和 resume/session_id
OpenAI Agents SDK
openaiSession(sessionId, { maxItems }) 返回 OpenAI Agents SDK 的 Session 协议实现(getItems / addItems / popItem / clearSession)。// openaiSession 会以 conversation id 为依据自动恢复会话// typescriptconst session = context.store.openaiSession(context.conversation_id)// pythonsession = context.store.openai_session(context.conversation_id)
断点续跑(审批流 / human-in-the-loop)要额外用
store.state。 上面的 openaiSession 只持久化对话上下文,**不持久化中断时的 RunState**。所以当工具需要人工批准、run() 在中途中断时,要自己把 RunState 序列化存进 context.store.state,下一轮读回、还原、再 run():// agents/hitl/index.tsimport { RunState, run } from '@openai/agents'const RUN_STATE_KEY = 'openai.run-state'// —— 首轮:跑起来,如果中断就把 RunState 存进 state ——const result = await run(agent, message, { signal })if (result.state.getInterruptions().length > 0) {await context.store.state.set(RUN_STATE_KEY, result.state.toString()) // 关键:存 RunStatereturn json({ status: 'awaiting_approval' })}await context.store.state.delete(RUN_STATE_KEY) // 没中断,跑完了,清掉// —— 下一轮:带批准/拒绝进来,读回 RunState 还原、续跑 ——const stored = await context.store.state.get<string>(RUN_STATE_KEY)const state = await RunState.fromString(agent, stored) // 还原中断态const [approval] = state.getInterruptions()body.approved ? state.approve(approval) : state.reject(approval)const resumed = await run(agent, state, { signal }) // 从断点续跑
LangGraph
langgraphCheckpointer 实现 BaseCheckpointSaver(getTuple / list / put / putWrites),langgraphStore 实现 BaseStore(get / put / search / listNamespaces / batch),BaseStore的 search 暂未支持按语义检索。// typescriptconst checkpointer = context.store.langgraphCheckpointerconst store = context.store.langgraphStore// pythoncheckpointer = context.store.langgraph_checkpointerstore = context.store.langgraph_store
DeepAgents
DeepAgents 基于 LangGraph 构建,直接复用
langgraphCheckpointer + langgraphStore 两个适配器。// typescriptconst checkpointer = context.store.langgraphCheckpointerconst store = context.store.langgraphStore// pythoncheckpointer = context.store.langgraph_checkpointerstore = context.store.langgraph_store
CrewAI
CrewAI 自带 Memory 强依赖向量库(默认 LanceDB + embedder),
context.store暂时未建向量层,对话历史用 context.store通用 API 自管即可,多角色编排、任务链、工具调用等核心能力完全不受影响。如果需要跨实例恢复会话,每轮结束把框架的会话状态序列化成 JSON 存进去,下一轮开始先读回来还原。只要状态能表达成可 JSON 序列化的对象,就能实现同样的跨实例多轮记忆和断点续跑。典型落地是审批流(human-in-the-loop)——工具调用需人工批准时把运行状态存进
state、返回「等待审批」,下次带同一个 conversation_id(批准或拒绝)读回状态、从断点接着跑,跑完删掉;序列化状态始终只留在服务端。通用 API
方法总览
方法均挂在context.store上。Node 端使用对象解构入参(camelCase),Python 端使用关键字参数(snake_case),语义完全一致。
Node 方法 | Python 方法 | 描述 |
appendMessage | append_message | 追加一条消息;对话不存在时自动创建 |
getMessages | get_messages | 拉消息列表,支持游标分页 |
updateMessage | update_message | 覆盖式更新指定消息 |
deleteMessage | delete_message | 删除单条消息 |
clearMessages | clear_messages | 清空消息但保留对话元信息 |
getConversation | get_conversation | 取对话元信息 |
listConversations | list_conversations | 列对话,按 lastMessageAt 倒序 |
updateConversation | update_conversation | 更新对话元信息,浅合并 |
deleteConversation | delete_conversation | 删除整个对话(不可恢复) |
toAnthropicMessages | to_anthropic_messages | 把消息列表转成 Anthropic Messages 格式 |
toOpenAIInput | to_openai_input | 把消息列表转成 OpenAI Chat Completions 格式 |
appendMessage / append_message
向指定对话追加一条消息,对话不存在时自动创建,并按需建立用户索引。
参数
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 业务侧对话 ID,长度 ≤ 256 字节 |
role | 'user' | 'assistant' | 'system' | 'tool' | Yes | 消息角色 |
content | string | string[] | object | Yes | 消息正文,支持纯文本 / 字符串数组 / 多模态 dict,序列化后 ≤ 50MB |
metadata | Record<string, any> | No | 业务自定义字段(token 数、tool_call、来源标签等) |
userId / user_id | string | No | 关联用户,传入后写入用户索引,便于按用户列对话 |
返回值
新消息的
messageId / message_id(形如 msg_xxx)。TS 示例:
const messageId = await context.store.appendMessage({conversationId: context.conversation_id,role: 'user',content: context.request.body.message,userId: context.request.body.userId,metadata: { source: 'web' },})
Python 示例:
message_id = await context.store.append_message(conversation_id=context.conversation_id,role="user",content=context.request.body["message"],user_id=context.request.body.get("user_id"),metadata={"source": "web"},)
getMessages / get_messages
拉取指定对话的消息列表,支持游标分页。对话不存在不抛异常,返回空列表,默认按时间正序(
order='asc',最早优先),方便直接拼 prompt。参数
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 对话 ID |
limit | number | No | 单页条数,默认 20,范围 [1, 100] |
order | 'asc' | 'desc' | No | 排序方向,默认 'asc'(最早优先) |
after | string | No | 游标,取该 messageId 之后的消息;与 before 互斥 |
before | string | No | 游标,取该 messageId 之前的消息;与 after 互斥 |
返回值
list[Message] —— 消息数组,对话不存在返回 []。TS 示例:
const messages = await context.store.getMessages({conversationId: context.conversation_id,limit: 50,})const reply = await openai.chat.completions.create({model: 'gpt-4o',messages: context.store.toOpenAIInput(messages),})
Python 示例:
messages = await context.store.get_messages(conversation_id=context.conversation_id,limit=50,)reply = await openai_client.chat.completions.create(model="gpt-4o",messages=context.store.to_openai_input(messages),)
updateMessage / update_message
覆盖式更新一条消息,仅传入的字段会被覆盖,未传字段保持原值;
updatedAt / updated_at 自动刷新。参数
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 对话 ID |
messageId / message_id | string | Yes | 目标消息 ID |
content | string | string[] | object | No | 新正文;不传则保持原值 |
metadata | Record<string, any> | No | 整体覆盖 metadata(非合并);不传则保持原值 |
返回值
Message —— 更新后的完整消息对象。TS 示例:
const updated = await context.store.updateMessage({conversationId: context.conversation_id,messageId: 'msg_abc123',content: 'corrected answer',metadata: { edited: true },})
Python 示例:
updated = await context.store.update_message(conversation_id=context.conversation_id,message_id="msg_abc123",content="corrected answer",metadata={"edited": True},)
deleteMessage / delete_message
删除单条消息。
参数
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 对话 ID |
messageId / message_id | string | Yes | 目标消息 ID |
TS 示例:
await context.store.deleteMessage({conversationId: context.conversation_id,messageId: 'msg_abc123',})
Python 示例:
await context.store.delete_message(conversation_id=context.conversation_id,message_id="msg_abc123",)
clearMessages / clear_messages
清空对话中所有消息,但保留
ConversationMeta,彻底删除请用 deleteConversation。参数
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 对话 ID |
TS 示例:
await context.store.clearMessages({ conversationId: context.conversation_id })
Python 示例:
await context.store.clear_messages(conversation_id=context.conversation_id)
getConversation / get_conversation
获取对话元信息。
参数
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 对话 ID |
返回值
ConversationMeta —— 包含 conversationId / createdAt / lastMessageAt / messageCount / metadata 等字段。TS 示例:
const meta = await context.store.getConversation({conversationId: context.conversation_id,})console.log(meta.messageCount, meta.metadata?.title)
Python 示例:
meta = await context.store.get_conversation(conversation_id=context.conversation_id,)print(meta.message_count, (meta.metadata or {}).get("title"))
listConversations / list_conversations
列对话,按
lastMessageAt 排序,支持游标分页与按用户过滤。
参数
Parameter | Type | Required | Description |
limit | number | No | 单页条数,默认 20,范围 [1, 100] |
order | 'asc' | 'desc' | No | 排序方向,默认 'desc'(最新优先) |
after | string | No | 游标,传上一页返回的 nextCursor |
before | string | No | 游标,传上一页返回的 previousCursor |
userId / user_id | string | No | 仅列该用户名下的对话(命中用户索引) |
返回值
ListConversationsResult —— { items, nextCursor, previousCursor }。
TS 示例:
const { items, nextCursor } = await context.store.listConversations({userId: 'u_123',limit: 20,})
Python 示例:
result = await context.store.list_conversations(user_id="u_123", limit=20)items, next_cursor = result.items, result.next_cursor
updateConversation / update_conversation
浅合并 metadata:同 key 覆盖、不同 key 保留。
参数
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 对话 ID |
metadata | Record<string, any> | Yes | 待合并字段;value 为 null / None 表示删除该 key |
返回值
ConversationMeta —— 合并后的对话元信息。TS 示例:
await context.store.updateConversation({conversationId: context.conversation_id,metadata: { title: '产品咨询', tag: null }, // 设标题,删 tag})
Python 示例:
await context.store.update_conversation(conversation_id=context.conversation_id,metadata={"title": "产品咨询", "tag": None},)
deleteConversation / delete_conversation
删除整个对话,同步清理消息索引、会话元信息和全局会话索引,不可恢复。
参数:
Parameter | Type | Required | Description |
conversationId / conversation_id | string | Yes | 对话 ID |
TS 示例:
await context.store.deleteConversation({conversationId: context.conversation_id,})
Python 示例:
await context.store.delete_conversation(conversation_id=context.conversation_id,)
toAnthropicMessages / to_anthropic_messages
把
getMessages 返回的消息列表转成 Anthropic Messages API 的 messages 字段格式。参数
Parameter | Type | Required | Description |
messages | Message[] | Yes | getMessages / get_messages 的返回结果 |
返回值
Array<{ role: string; content: unknown }>—— 可直接作为 anthropic.messages.create({ messages }) 的入参。TS 示例:
const history = await context.store.getMessages({conversationId: context.conversation_id,})const resp = await anthropic.messages.create({model: 'claude-sonnet-4',max_tokens: 1024,messages: context.store.toAnthropicMessages(history),})
Python 示例:
history = await context.store.get_messages(conversation_id=context.conversation_id,)resp = await anthropic_client.messages.create(model="claude-sonnet-4",max_tokens=1024,messages=context.store.to_anthropic_messages(history),)
toOpenAIInput / to_openai_input
把
getMessages 返回的消息列表转成 OpenAI Chat Completions API 的 messages 字段格式,保留 role ∈ user / assistant / system / tool,content 透传。参数
Parameter | Type | Required | Description |
messages | Message[] | Yes | getMessages / get_messages 的返回结果 |
返回值
Array<{ role: string, content: any }> —— 可直接作为 openai.chat.completions.create({ messages }) 的入参。TS 示例:
const history = await context.store.getMessages({conversationId: context.conversation_id,})const resp = await openai.chat.completions.create({model: 'gpt-4o',messages: context.store.toOpenAIInput(history),})
Python 示例:
history = await context.store.get_messages(conversation_id=context.conversation_id,)resp = await openai_client.chat.completions.create(model="gpt-4o",messages=context.store.to_openai_input(history),)
state — 通用状态存储
前面的方法都是围绕「消息」的。
context.store.state 是另一类能力:一个按对话隔离的 JSON 键值存储,存什么由你决定。适合任何框架,尤其是没有原生适配器的框架(如 CrewAI)、以及要存业务自定义状态(步骤计数、临时变量、序列化后的框架运行状态)的场景。conversationId 自动绑定当前对话,方法上不用再传,写入按 conversationId 命名空间隔离。和消息历史、会话上下文一样,
state 里的值也是跨实例持久化的——换实例读同一个 conversationId 仍在。Node 方法 | Python 方法 | 描述 |
state.get(key) | state.get(key) | 读取;key 不存在返回 null / None |
state.set(key, value) | state.set(key, value) | 写入;value 必须 JSON 可序列化 |
state.delete(key) | state.delete(key) | 删除该 key |
三个方法均为异步。
参数与约束
项 | 说明 |
key | 字符串,长度 1–256 |
value | 必须 JSON 可序列化: null / 字符串 / 布尔 / 有限数字 / 普通对象 / 数组。函数、循环引用、NaN / Infinity 会被拒绝 |
TS 示例:
// 请求开始:读回上一轮状态(没有则 null)const saved = await context.store.state.get<MyState>('session')// ... 跑一轮对话 ...// 请求结束:把最新状态存回去await context.store.state.set('session', latestState)
Python 示例:
saved = await context.store.state.get("session")# ... 跑一轮 ...await context.store.state.set("session", latest_state)
通用做法:用其他框架实现多轮记忆 / 断点续跑。 没有原生适配器时,思路是每轮结束把框架的会话状态序列化成 JSON 存进
state,下一轮开始先读回来还原。只要状态能表达成可 JSON 序列化的对象,就能实现跨进程的多轮记忆和断点续跑,不依赖任何框架特定的适配器。一个典型落地是审批流(human-in-the-loop):工具调用需要人工批准时,把框架序列化出的运行状态存进 state,返回「等待审批」;下一次带同一个 conversationId 进来(批准或拒绝),读回状态、从断点接着跑,跑完删掉。序列化的状态始终只留在服务端,前端只发消息和「批准/拒绝」的决定。数据结构
interface Message {messageId: string // msg_xxx(appendMessage 自动生成)role: 'user' | 'assistant' | 'system' | 'tool'content: any // string / array / object(多模态)createdAt: number // ms 时间戳metadata?: Record<string, any> // 自定义(token 数、tool_call 等)updatedAt?: number // 仅 updateMessage 后存在}interface ConversationMeta {conversationId: stringcreatedAt: number // ms 时间戳lastMessageAt: number // ms 时间戳messageCount: numbermetadata?: Record<string, any> // 业务自定义(标题、用户、标签等)}interface ListConversationsResult {items: ConversationMeta[]nextCursor?: string // 下一页游标,传给 after 参数previousCursor?: string // 上一页游标,传给 before 参数}
Python 端字段为message_id / conversation_id / created_at / last_message_at / message_count / next_cursor / previous_cursor,整体形态一致。
限制与配额
项 | 默认 | 超限行为 |
conversation_id 长度 | ≤ 256 字符 | 抛 MemoryValidationError |
单条 content 大小 | ≤ 50MB(序列化后) | 抛 MemoryValidationError |
单对话最大消息数 | 10000 | 抛 MemoryQuotaExceededError |
limit 上限 | 100 | 抛 MemoryValidationError |
limit 下限 | 1 | 抛 MemoryValidationError |
单条
content 超大(长文档、图片原始数据)建议放对象存储,传 URL 进消息。对话摘要
getMessages 单次最多取 100 条,长对话下推荐做法是保留最近 N 条原文 + 早期内容压成一段摘要,每次请求时从 Store 拼装回 prompt。Store 本身不调 LLM、不生成摘要,只提供 ConversationMeta.metadata 这个 JSON 容器。职责切分
谁负责 | 做什么 |
Store | 提供 metadata JSON 字段 + 浅合并写入。约定两个 key:summary(摘要文本)、summarizedUntil(已摘要到哪条 messageId) |
业务侧 | 决定何时摘要、调便宜模型生成摘要、调 updateConversation 写回 |
summary/summarizedUntil是约定 key 而非 schema,Store 只把它当任意 JSON 存。你可以换名字,只要业务侧前后保持一致。
实现示例
可以使用便宜模型做摘要——压缩 + 保关键事实不需要主模型那种推理能力。prompt 中四条要求都不能省,否则会出现摘要发散、寒暄混入、语言乱跳、越摘越长等问题。
async function summarizeWithLLM(previousSummary, newMessages) {const transcript = newMessages.filter(m => m.role === 'user' || m.role === 'assistant').map(m => `${m.role}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`).join('\n')const prompt = previousSummary? `下面是一段对话的【已有摘要】和【新增对话】,请合并成一段新摘要。要求:- 保留:关键事实、用户偏好、未完成的任务、重要的上下文设定- 删除:寒暄、确认收到、重复内容、工具调用细节- 用与最近对话相同的语言书写- 控制在 500 字内【已有摘要】${previousSummary}【新增对话】${transcript}【新摘要】`: `请把下面的对话总结成一段摘要。要求:- 保留:关键事实、用户偏好、未完成的任务、重要的上下文设定- 删除:寒暄、确认收到、重复内容、工具调用细节- 用与对话相同的语言书写- 控制在 500 字内【对话】${transcript}【摘要】`const { text } = await generateText({model: openai('gpt-4o-mini'),prompt,maxTokens: 800,})return text.trim()}
消息存储和会话上下文存储的区别
context.store 上有两类互相独立、不能互相替代的持久化,多数对话应用两个都要:存哪种 | 管什么效果 | 怎么存 |
消息历史 | 前端刷新后聊天记录还在、能重新渲染,侧边栏能列历史会话 | 通用 API appendMessage / getMessages,是纯读写、不调模型的数据 |
会话上下文 | 模型记得上一轮说了什么(多轮连续对话)、中断能续跑 | 交给框架原生适配器(见「跨实例恢复会话」),框架自己按原生格式读写 |
两个最常见的误区:
只写了消息历史、没接会话上下文 →
/history 里看得到记录,但模型每轮都失忆,因为上下文没喂回给它。只接了会话上下文、没写消息历史 → 模型记得住,但前端一刷新聊天记录空白,因为框架的 session 格式不是给前端渲染用的。
一句话:消息历史管「刷新后看得见」,会话上下文管「模型记得住」,各管一摊,都是跨实例持久化的。