第 10 章 · 会话管理 session state 与 chat history
本章目标:分清"聊天历史"与"会话状态"两套机制,掌握
session_id、add_history_to_context与session_state的用法,理解工具如何通过RunContext读写状态。
10.1 三个容易混淆的概念
多轮对话应用有三个"记忆"层次,官方文档用一张表讲清了它们的边界:
| 机制 | 存什么 | 作用域 | 典型用途 |
|---|---|---|---|
| Chat history(聊天历史) | 之前 run 的消息与工具调用 | session_id | 同一会话内的对话连贯性 |
| Session state(会话状态) | 代码/工具管理的应用数据 | session_id | 购物车、任务清单、计数器 |
| Memory(用户记忆) | 提炼出的用户事实 | user_id(跨会话) | 偏好、画像(第 12 章) |
一句话总结:history 是"聊过什么",state 是"办到哪了",memory 是"这人是谁"。本章聚焦前两者。
10.2 session_id 与聊天历史
session_id 是一次对话线程的身份证。同一个 session_id 的多次 run 属于同一段对话。要让模型"记得上文",必须同时满足两个条件:
- 配置数据库——历史消息要落库(
db=SqliteDb(...),下一章细讲); - 开启历史注入——
add_history_to_context=True。
# chat_history.py —— 多轮对话记忆
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
db=SqliteDb(db_file="tmp/agent.db"), # 存储会话 run
add_history_to_context=True, # 每次运行注入历史消息
num_history_runs=3, # 只带最近 3 轮
markdown=True,
)
agent.print_response("我叫小高,我在准备 FastAPI 面试", session_id="chat_001")
agent.print_response("我叫什么?我在准备什么?", session_id="chat_001") # 能答上
agent.print_response("我叫什么?", session_id="chat_002") # 换会话:答不上历史注入是有成本的:每轮都把历史消息塞进上下文,token 随对话长度增长。官方提供了三个控制阀门:
| 参数 | 说明 |
|---|---|
num_history_runs | 注入最近几轮 run(默认 3) |
num_history_messages | 按消息条数封顶(与上面二选一,同时设置时 num_history_runs 生效) |
max_tool_calls_from_history | 限制历史中工具调用消息的数量 |
按需回查的写法只需一行配置,效果立竿见影:
# on_demand_history.py —— 让模型自己决定何时回看历史
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
db=SqliteDb(db_file="tmp/agent.db"),
read_chat_history=True, # 模型获得 get_chat_history() 工具
# 注意:没有 add_history_to_context=True,普通提问不携带历史
)
agent.print_response("我们之前约定过什么暗号吗?", session_id="chat_001")
# 模型判断需要上下文 -> 调用 get_chat_history() -> 基于结果回答这种模式的适用面很讲究:适合审计、分析类场景(大多数提问不依赖上文,偶尔要翻旧账);如果是每句话都带指代的闲聊场景,老老实实开 add_history_to_context 反而延迟更低——按需回查要多花一轮工具调用。
省钱的另一种思路
如果大多数提问其实不依赖上文,可以不开历史注入,改为 read_chat_history=True——这会给模型一个 get_chat_history() 工具,让它按需回查历史,而不是每轮都全量携带。
10.3 会话状态:给 Agent 一块"黑板"
session_state 是一个挂在会话上的 dict,初始值由 Agent(session_state={...}) 提供。它和聊天历史的本质区别:state 由你的代码(通常是工具)读写,而不是由模型的消息自然累积。
# session_state_basic.py —— 官方购物清单示例的三方模型版
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat
from agno.run import RunContext
def add_item(run_context: RunContext, item: str) -> str:
"""Add an item to the shopping list.
Args:
item: The item to add to the shopping list.
"""
# run_context.session_state 就是当前会话的状态字典
if run_context.session_state is None:
run_context.session_state = {}
run_context.session_state["shopping_list"].append(item)
return f"The shopping list is now {run_context.session_state['shopping_list']}"
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
db=SqliteDb(db_file="tmp/agent.db"),
session_state={"shopping_list": []}, # 新会话的初始状态
tools=[add_item],
# instructions 里可以用 {键名} 引用状态,注入时会做模板替换
instructions="Current state (shopping list) is: {shopping_list}",
markdown=True,
)
agent.print_response("帮我把牛奶、鸡蛋、面包加进购物清单", session_id="s_100")
print(f"Final session state: {agent.get_session_state()}") # 代码侧读取三个关键机制:
RunContext注入:工具函数声明run_context: RunContext参数后,框架执行时自动传入,且该参数不会出现在发给模型的 schema 里(第 7 章埋的伏笔在这里兑现);- 自动持久化:对
run_context.session_state的修改会随会话写入数据库,同session_id的下一次 run 自动加载; - instructions 模板:
{shopping_list}这样的占位符会在注入时替换为状态的实际值,让模型"看得见"当前状态。
10.4 多会话隔离实验
理解 state 作用域最好的方式是亲手做隔离实验:
# state_isolation.py —— 验证 state 按 session 隔离
import os
from agno.agent import Agent
from agno.db.sqlite import SqliteDb
from agno.models.openai import OpenAIChat
from agno.run import RunContext
def set_counter(run_context: RunContext, value: int) -> str:
"""Set the counter to a value.
Args:
value: New counter value.
"""
run_context.session_state["counter"] = value
return f"counter = {value}"
agent = Agent(
model=OpenAIChat(
id="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
),
db=SqliteDb(db_file="tmp/agent.db"),
session_state={"counter": 0},
tools=[set_counter],
instructions="counter 的当前值是: {counter}",
)
agent.print_response("把计数器设为 42", session_id="session_A")
agent.print_response("计数器现在是多少?", session_id="session_A") # 42
agent.print_response("计数器现在是多少?", session_id="session_B") # 0(初始值)session_A 与 session_B 的 state 互不可见——这正是"每个用户/每个工单一条会话"的隔离基础。此外,run() 时也可以临时传入 session_state 覆盖默认值(优先级高于 Agent 构造参数);enable_agentic_state=True 还能进一步让模型自己按 JSON 补丁协议更新状态,适合让 Agent 自主维护复杂任务进度。
10.5 选型:什么时候用 history,什么时候用 state
- 对话语气、指代消解("它多少钱?""刚才说的第二点展开讲讲")→ chat history;
- 业务进度(购物车内容、审批到哪一步、已重试次数)→ session state;
- 跨会话的用户偏好("这位用户喜欢简洁回复")→ memory(第 12 章)。
一个客服 Agent 的标准组合拳:memory 记客户沟通偏好,history 维持当前工单对话连贯,state 存工单处理状态。三者各司其职,互不替代。
本章小结
session_id划定一次对话线程;history/state 都以它为作用域,memory 以user_id为作用域;- 聊天记忆 = 数据库 +
add_history_to_context=True;token 预算用num_history_runs/num_history_messages控制,或改用read_chat_history=True按需回查; session_state由代码(工具)通过run_context.session_state读写,修改自动落库;instructions 中用{键}引用状态;- state 天然按 session 隔离,是构建多用户/多工单应用的基石。
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. 让模型在多轮对话中记住上文,最少需要哪两个配置?
2. num_history_runs 和 num_history_messages 同时设置时会怎样?
3. 工具函数中的 run_context.session_state 修改后会发生什么?
4. instructions 中写 "当前清单: {shopping_list}",这个占位符何时生效?
🛠️ 动手实践
- 把 10.3 的购物清单扩展出
remove_item(item: str)工具,测试"把鸡蛋去掉"能否正确删除,并观察 instructions 中{shopping_list}的实时变化。 - 构造一段 10 轮以上的对话,分别用
num_history_runs=1和=5运行,对比response.metrics中 token 用量的差异。 - 实现一个"多轮订票助手":用 session_state 保存出发地/目的地/日期三个字段,只有三者齐备才调用
book_ticket()工具(提示:在工具内校验 state)。
完成动手实践后,进入第 11 章:存储 Storage——会话持久化。