上篇你把私厨 Demo 拼起来了——看图、工具、会话记忆都能跑。可「这通对话的计数」「跨会话的饮品偏好」「当前登录用户是谁」常被揉进同一个 list。这篇讲 Runtime 三件套:State / Store / Context,并在 Tool 里正确访问它们。
小明把三种「记住」混成一锅
私厨上线试玩后,产品又提需求:
1) 本会话点了几次「换一道」→ 显示 visit_count
2) 用户喜欢少油 → 换个 thread 还要记得
3) 同一套 Agent 服务小明和小华 → 别串档案
小明全往 ConversationMemory 里塞。换 thread_id 后偏好丢了;不换 thread,两个用户的忌口又串了。
他去找老张:”都是记忆,为啥不能一个 list 搞定?”
老张说:”因为生命周期不一样。Runtime 三件套各管一段:
State = 这通电话聊到哪(含本轮任务字段)
Store = 客户档案柜(跨电话线还能查)
Context = 这次是谁打进来的(user_id 等配置)
“
什么是 Runtime 三分法
压成一张表:
| 概念 |
说明 |
生命周期 |
工具里怎么拿 |
| State |
短期:messages + 自定义字段 |
单 thread_id |
runtime.state / Command |
| Store |
长期:偏好、可检索笔记 |
跨会话 / 跨 thread |
runtime.store.get/put/search |
| Context |
本次调用配置(如登录用户) |
单次请求 |
runtime.context |
invoke(messages, config.thread_id, context=user_id)
│
├─► State(本 thread 消息与计数)
├─► Context(这次是谁)
└─► Store(长期档案 / 向量笔记)
▲
│ 工具经 ToolRuntime 读写
“比喻:
-
State = 白板(这节课写满,下课可擦,同教室续写靠 thread)
-
-
Context = 工牌(告诉工具现在接待的是哪位客户)”
| 需求 |
该放哪 |
| 「我叫什么?」同会话追问 |
State.messages |
| 本会话 visit_count |
State 自定义字段 |
| 「喜欢龙井」换会话仍在 |
Store |
| 按「喝茶和西湖」语义搜笔记 |
Store + 向量 index |
| 区分 user_xiao / user_hua |
Context.user_id |
代码拆解:State → Store → Context
1. State:自定义短期字段
默认 AgentState 主要是 messages。要记「会话开始时间、访问次数」:
class CustomState(AgentState):
session_start: NotRequired[str]
visit_count: NotRequired[int]
model_call_count: NotRequired[int]
创建时挂上:
return create_agent(
get_chat_model(),
tools=CUSTOM_TOOLS,
system_prompt="……",
state_schema=CustomState,
checkpointer=InMemorySaver(),
)
工具里读 / 写:
@tool
def get_session_info(runtime: ToolRuntime) -> str:
state = runtime.state
return f"session_start={state.get('session_start')}; visit_count={...}"
@tool
def bump_visit(runtime: ToolRuntime) -> Command:
count = int(runtime.state.get("visit_count") or 0) + 1
return Command(update={"visit_count": count, "messages": [...]})
“runtime 是框架注入参数,模型看不见;业务参数别叫这个名。写回用 Command(update=...)。”
2. Store:跨 thread 的长期记忆
键值结构:namespace + key + JSON value。
USERS_NS = ("users",)
def make_store() -> InMemoryStore:
return InMemoryStore()
store.put(USERS_NS, "user_xiao", {"name": "小明", "drink": "龙井茶"})
Agent 注入:
def build_agent(*, store: BaseStore):
return create_agent(
get_chat_model(),
tools=PROFILE_TOOLS,
system_prompt="需要用户档案时调用 get_user_profile;……",
store=store,
context_schema=Context,
checkpointer=InMemorySaver(),
)
工具经 runtime.store + runtime.context.user_id:
@tool
def get_user_profile(runtime: ToolRuntime[Context]) -> str:
user_id = runtime.context.user_id
item = runtime.store.get(USERS_NS, user_id)
...
@tool
def save_user_profile(name: str, drink: str, runtime: ToolRuntime[Context]) -> str:
user_id = runtime.context.user_id
runtime.store.put(USERS_NS, user_id, {"name": name, "drink": drink})
return f"已保存档案 user_id={user_id}: ..."
“Demo B 的故事:thread-1 写入偏好 → thread-2 换短期会话 → 同一 Context(user_id=...) 仍能读出档案。这就是 Store 相对 State 的价值。”
3. 向量 Store:语义检索长期笔记
导图:文字→向量,相近含义在空间中更近。课件用 text-embedding-v4:
def make_vector_store() -> InMemoryStore:
return InMemoryStore(
index={
"dims": EMBED_DIMS,
"embed": embed_texts,
"fields": ["text"],
}
)
@tool
def search_memories(query: str, runtime: ToolRuntime[Context]) -> str:
hits = runtime.store.search(MEMORIES_NS, query=query, limit=2)
...
4. Context:本次是谁
@dataclass
class Context:
user_id: str
调用时传入(与 thread 分开):
agent.invoke(
{"messages": [{"role": "user", "content": "我的档案里写了什么?"}]},
{"configurable": {"thread_id": "day10-a"}},
context=Context(user_id="user_xiao"),
)
“导图例子:用户先登录再访问 Agent——登录身份进 Context,工具用 runtime.context 取,再去 Store 里按 user_id 取档案。别把 user_id 只写在某句聊天里碰运气。“
小明复述:”State 管这通;Store 管档案;Context 管工牌;工具统一走 ToolRuntime。”
总结
老张说:”第九篇只办一件事——把三种『记住』拆开。”
“三个核心理解:
-
State —— 本会话消息与任务字段;
state_schema + runtime.state
-
Store —— 跨会话键值 / 向量;
store= + runtime.store
-
Context —— 本次工牌;
context_schema + runtime.context“
LangChain 支线进度:
… → AI 私厨实战
↓
Runtime:State / Store / Context(本篇)
↓
Middleware → 多 Agent → Agentic RAG …
小明说:”下一篇该看中间件了——在模型调用前后挂钩子,做限流、摘要、脱敏。”
“对。下一篇:Middleware。”
Runtime 三分法:白板记这通,档案柜记长期,工牌标明此刻是谁。混用一个 list,不是省事,是埋雷。