Skip to content

第 10 章 · 会话管理 session state 与 chat history

本章目标:分清"聊天历史"与"会话状态"两套机制,掌握 session_idadd_history_to_contextsession_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 属于同一段对话。要让模型"记得上文",必须同时满足两个条件:

  1. 配置数据库——历史消息要落库(db=SqliteDb(...),下一章细讲);
  2. 开启历史注入——add_history_to_context=True
python
# 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限制历史中工具调用消息的数量

按需回查的写法只需一行配置,效果立竿见影:

python
# 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 由你的代码(通常是工具)读写,而不是由模型的消息自然累积

python
# 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()}")   # 代码侧读取

三个关键机制:

  1. RunContext 注入:工具函数声明 run_context: RunContext 参数后,框架执行时自动传入,且该参数不会出现在发给模型的 schema 里(第 7 章埋的伏笔在这里兑现);
  2. 自动持久化:对 run_context.session_state 的修改会随会话写入数据库,同 session_id 的下一次 run 自动加载;
  3. instructions 模板{shopping_list} 这样的占位符会在注入时替换为状态的实际值,让模型"看得见"当前状态。

10.4 多会话隔离实验

理解 state 作用域最好的方式是亲手做隔离实验:

python
# 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_Asession_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}",这个占位符何时生效?

🛠️ 动手实践

  1. 把 10.3 的购物清单扩展出 remove_item(item: str) 工具,测试"把鸡蛋去掉"能否正确删除,并观察 instructions 中 {shopping_list} 的实时变化。
  2. 构造一段 10 轮以上的对话,分别用 num_history_runs=1=5 运行,对比 response.metrics 中 token 用量的差异。
  3. 实现一个"多轮订票助手":用 session_state 保存出发地/目的地/日期三个字段,只有三者齐备才调用 book_ticket() 工具(提示:在工具内校验 state)。

完成动手实践后,进入第 11 章:存储 Storage——会话持久化