第 6 章:上下文与会话记忆

系列教程:OpenAI Agents SDK 从入门到实战

本章目标:搞懂 RunContext(依赖注入)和 Session(跨轮记忆),让你的 Agent 既能访问业务数据,又能记住聊过的话。


为什么需要上下文和记忆?

Agent 默认是"金鱼记忆"。每次调用 Runner.run(),它都从零开始,既不记得之前聊过什么,也拿不到你应用里的任何数据。

这带来两个问题:

  1. 拿不到业务数据:工具函数需要访问数据库连接、当前用户信息、API 客户端等,但 Agent 不知道这些东西在哪
  2. 记不住对话历史:问完"我叫小明",再问"我叫什么",AI 一脸茫然

SDK 用两套机制分别解决这两个问题:

问题 解决方案 类比
工具函数需要外部依赖 RunContext 像 FastAPI 的依赖注入
跨轮对话需要记忆 Session 像浏览器的 cookie/session

它们各司其职,互不冲突,也可以一起用。


RunContext:给 Agent 注入运行时依赖

问题场景

假设你在做一个航空客服系统,工具函数需要知道当前乘客是谁、航班号是什么。你不能把这些硬编码进去,因为每个用户不一样。

笨办法是用全局变量,但那太丑了,也不安全。RunContext 的思路是:定义一个上下文对象,传给 Runner.run(),工具函数里自动就能拿到。

定义 Context 类型

Context 可以是任何类型,最常用的是 Pydantic BaseModel 或 dataclass:

from pydantic import BaseModel

class CustomerContext(BaseModel):
    """客服系统的运行时上下文"""
    customer_id: str
    customer_name: str
    vip_level: int = 0
    db_connection: object = None  # 数据库连接之类的也可以塞进来

    class Config:
        arbitrary_types_allowed = True  # 允许非标准类型

用 dataclass 也行:

from dataclasses import dataclass

@dataclass
class CustomerContext:
    customer_id: str
    customer_name: str
    vip_level: int = 0

在工具函数中访问 Context

关键角色是 RunContextWrapper。工具函数的第一个参数如果声明为 RunContextWrapper[YourContext],SDK 会自动注入:

from agents import RunContextWrapper, function_tool

@function_tool
async def check_order(
    context: RunContextWrapper[CustomerContext],  # SDK 自动注入,不算工具参数
    order_id: str,                                # 这个才是 LLM 填的参数
) -> str:
    """查询订单状态"""
    # 通过 context.context 访问你的自定义上下文
    customer = context.context
    # 这里可以用 customer.db_connection 查数据库
    return f"客户 {customer.customer_name} 的订单 {order_id} 状态:已发货"

注意:context 参数对 LLM 是不可见的,LLM 不知道它的存在,也不需要填它。LLM 只看到 order_id 这个参数。

Agent 的泛型标注

Agent[YourContext] 告诉类型检查器,这个 Agent 用的是哪种 Context:

from agents import Agent

agent = Agent[CustomerContext](
    name="客服助手",
    instructions="你是一个客服助手,帮助客户查询订单。",
    tools=[check_order],
)

这个泛型标注主要是给 IDE 和 mypy 用的,让它们能正确做类型推断。不写也能运行,但写了会更安全。

运行时传入 Context

from agents import Runner

# 创建上下文实例,填入当前用户的信息
ctx = CustomerContext(
    customer_id="C001",
    customer_name="张三",
    vip_level=2,
)

# 通过 context 参数传给 Runner.run()
result = await Runner.run(agent, "帮我查一下订单 ORD-12345", context=ctx)

整个流程就是:你创建 Context -> 传给 Runner -> SDK 包成 RunContextWrapper -> 工具函数里通过 context.context 拿到。

完整可运行示例:航空客服

import asyncio
import os

from openai import AsyncOpenAI
from pydantic import BaseModel

from agents import Agent, Runner, RunContextWrapper, OpenAIChatCompletionsModel, set_tracing_disabled, function_tool

# --- 模型配置 ---
set_tracing_disabled(True)
client = AsyncOpenAI(
    base_url=os.getenv("MODEL_BASE_URL", "http://localhost:8317/v1"),
    api_key=os.getenv("MODEL_API_KEY", "sk-12345678"),
)
model = OpenAIChatCompletionsModel(
    model=os.getenv("MODEL_NAME", "gpt-5.2"),
    openai_client=client,
)

# ========== 1. 定义上下文 ==========
class AirlineContext(BaseModel):
    """航空客服的运行时上下文"""
    passenger_name: str | None = None
    confirmation_number: str | None = None
    seat_number: str | None = None
    flight_number: str | None = None

# ========== 2. 定义工具,通过 context 访问数据 ==========
@function_tool
async def get_booking_info(
    context: RunContextWrapper[AirlineContext],
    confirmation_number: str,
) -> str:
    """根据确认号查询预订信息"""
    # 把确认号存到上下文里,后续工具可以复用
    context.context.confirmation_number = confirmation_number
    # 实际项目中这里会查数据库
    return (
        f"预订信息:确认号 {confirmation_number},"
        f"乘客 {context.context.passenger_name or '未知'},"
        f"航班 CA1234,座位 18A"
    )

@function_tool
async def change_seat(
    context: RunContextWrapper[AirlineContext],
    new_seat: str,
) -> str:
    """更换座位"""
    old_seat = context.context.seat_number or "18A"
    context.context.seat_number = new_seat
    return f"座位已从 {old_seat} 更换为 {new_seat}"

# ========== 3. 创建 Agent ==========
agent = Agent[AirlineContext](
    name="航空客服",
    instructions=(
        "你是航空公司客服助手。"
        "帮助乘客查询预订信息和更换座位。"
        "回答要简洁专业。"
    ),
    tools=[get_booking_info, change_seat],
    model=model,
)

# ========== 4. 运行 ==========
async def main():
    # 创建上下文,注入当前乘客信息
    ctx = AirlineContext(passenger_name="李明")

    # 第一次调用
    result = await Runner.run(
        agent,
        "帮我查一下确认号 ABC123 的预订信息",
        context=ctx,
    )
    print(f"客服: {result.final_output}")
    print(f"上下文中的确认号: {ctx.confirmation_number}")  # 被工具更新了

    # 第二次调用(注意:这里没有 Session,Agent 不记得上一轮对话)
    result = await Runner.run(
        agent,
        "帮我把座位换到 22B",
        context=ctx,  # 但上下文还在,工具能拿到
    )
    print(f"客服: {result.final_output}")
    print(f"上下文中的座位: {ctx.seat_number}")

if __name__ == "__main__":
    asyncio.run(main())

Context 是跨工具调用共享的 -- 一个工具往里写数据,另一个工具能读到。但它不等于对话记忆,Agent 本身并不知道 Context 里有什么,Context 是给你的代码用的。


Session:跨轮对话记忆

问题场景

没有 Session 的 Agent 就像得了失忆症:

await Runner.run(agent, "我叫小明")
result = await Runner.run(agent, "我叫什么?")
print(result.final_output)  # "你还没告诉我你叫什么呢"

每次 Runner.run() 都是全新的一轮,AI 看不到之前的对话。

SQLiteSession:最简单的本地持久化

SDK 内置了 SQLiteSession,用 SQLite 存储对话历史:

from agents import SQLiteSession

# session_id:对话的唯一标识,不同 ID 就是不同的对话
# db_path:数据库文件路径,默认 ":memory:"(内存,程序退出就没了)
session = SQLiteSession("chat-001", db_path="conversations.db")

然后把它传给 Runner.run()

result = await Runner.run(agent, "我叫小明", session=session)
result = await Runner.run(agent, "我叫什么?", session=session)
print(result.final_output)  # "你叫小明"

就这样。传了同一个 session,SDK 会自动在每次 run 之前加载历史,run 结束后保存新内容。

Session 的工作原理

Runner.run(agent, input="你好", session=session)

内部发生了什么:
1. session.get_items()    -- 从数据库加载历史对话
2. 把历史 + 当前输入拼在一起发给模型
3. 模型返回结果
4. session.add_items()    -- 把本轮的输入和输出存起来

下次 run 的时候,第 1 步就能拿到之前所有记录。你不用操心任何细节。

完整示例:有记忆的多轮对话

import asyncio
import os

from openai import AsyncOpenAI

from agents import Agent, Runner, SQLiteSession, OpenAIChatCompletionsModel, set_tracing_disabled

# --- 模型配置 ---
set_tracing_disabled(True)
client = AsyncOpenAI(
    base_url=os.getenv("MODEL_BASE_URL", "http://localhost:8317/v1"),
    api_key=os.getenv("MODEL_API_KEY", "sk-12345678"),
)
model = OpenAIChatCompletionsModel(
    model=os.getenv("MODEL_NAME", "gpt-5.2"),
    openai_client=client,
)

agent = Agent(
    name="记忆助手",
    instructions="你是一个有记忆的AI助手,回答要简洁。",
    model=model,
)

async def main():
    # 创建 Session,数据存到文件里
    session = SQLiteSession("demo-chat", db_path="memory.db")

    # 第一轮:告诉 AI 一些信息
    result = await Runner.run(
        agent,
        "我叫小明,我是一名 Python 程序员,住在北京",
        session=session,
    )
    print(f"AI: {result.final_output}\n")

    # 第二轮:AI 应该记住名字
    result = await Runner.run(agent, "我叫什么?", session=session)
    print(f"AI: {result.final_output}\n")

    # 第三轮:AI 应该记住职业
    result = await Runner.run(agent, "我的职业是什么?", session=session)
    print(f"AI: {result.final_output}\n")

    # 第四轮:AI 应该记住城市
    result = await Runner.run(agent, "我住在哪里?", session=session)
    print(f"AI: {result.final_output}\n")

if __name__ == "__main__":
    asyncio.run(main())

预期输出类似:

AI: 你好小明!Python 程序员在北京,不错的组合。有什么我能帮你的?

AI: 你叫小明。

AI: 你是一名 Python 程序员。

AI: 你住在北京。

而且因为用了文件数据库,哪怕程序退出了再重新运行(只要 session_id 一样),AI 照样能记住。


手动管理历史 vs Session

在 Session 出现之前,多轮对话靠的是 result.to_input_list() 手动拼接:

旧方式:手动拼接

import asyncio
import os

from openai import AsyncOpenAI

from agents import Agent, Runner, OpenAIChatCompletionsModel, set_tracing_disabled

# --- 模型配置(省略详细注释,参考第1章)---
set_tracing_disabled(True)
client = AsyncOpenAI(
    base_url=os.getenv("MODEL_BASE_URL", "http://localhost:8317/v1"),
    api_key=os.getenv("MODEL_API_KEY", "sk-12345678"),
)
model = OpenAIChatCompletionsModel(
    model=os.getenv("MODEL_NAME", "gpt-5.2"),
    openai_client=client,
)

agent = Agent(name="助手", instructions="回答要简洁。", model=model)

async def main():
    # 第一轮
    result = await Runner.run(agent, "我叫小明")
    print(f"AI: {result.final_output}")

    # 手动把历史拼成新的输入
    input_items = result.to_input_list()
    input_items.append({"role": "user", "content": "我叫什么?"})

    # 第二轮,带上完整历史
    result = await Runner.run(agent, input_items)
    print(f"AI: {result.final_output}")

    # 第三轮,继续拼接...
    input_items = result.to_input_list()
    input_items.append({"role": "user", "content": "再说一遍?"})
    result = await Runner.run(agent, input_items)
    print(f"AI: {result.final_output}")

if __name__ == "__main__":
    asyncio.run(main())

新方式:Session

import asyncio
import os

from openai import AsyncOpenAI

from agents import Agent, Runner, SQLiteSession, OpenAIChatCompletionsModel, set_tracing_disabled

# --- 模型配置(省略详细注释,参考第1章)---
set_tracing_disabled(True)
client = AsyncOpenAI(
    base_url=os.getenv("MODEL_BASE_URL", "http://localhost:8317/v1"),
    api_key=os.getenv("MODEL_API_KEY", "sk-12345678"),
)
model = OpenAIChatCompletionsModel(
    model=os.getenv("MODEL_NAME", "gpt-5.2"),
    openai_client=client,
)

agent = Agent(name="助手", instructions="回答要简洁。", model=model)

async def main():
    session = SQLiteSession("my-chat")

    result = await Runner.run(agent, "我叫小明", session=session)
    print(f"AI: {result.final_output}")

    result = await Runner.run(agent, "我叫什么?", session=session)
    print(f"AI: {result.final_output}")

    result = await Runner.run(agent, "再说一遍?", session=session)
    print(f"AI: {result.final_output}")

if __name__ == "__main__":
    asyncio.run(main())

对比

维度 手动 to_input_list() Session
代码量 每轮都要拼接,容易写错 传个 session 就完事
持久化 没有,程序关了就丢了 可以存文件,跨进程保留
多用户 自己管理多个列表 不同 session_id 自动隔离
灵活性 完全自己控制 SDK 自动管理,也支持手动操作

to_input_list() 不是没用了,在一些需要精细控制输入的场景(比如你只想带部分历史),它依然有价值。但日常的多轮对话,Session 更省心。


完整实战:带 Context 和 Session 的聊天助手

把 RunContext 和 Session 组合起来,做一个既能访问业务数据、又有记忆的客服助手:

import asyncio
import os
from dataclasses import dataclass, field

from openai import AsyncOpenAI

from agents import Agent, Runner, RunContextWrapper, SQLiteSession, OpenAIChatCompletionsModel, set_tracing_disabled, function_tool

# --- 模型配置 ---
set_tracing_disabled(True)
client = AsyncOpenAI(
    base_url=os.getenv("MODEL_BASE_URL", "http://localhost:8317/v1"),
    api_key=os.getenv("MODEL_API_KEY", "sk-12345678"),
)
model = OpenAIChatCompletionsModel(
    model=os.getenv("MODEL_NAME", "gpt-5.2"),
    openai_client=client,
)

# ========== 上下文:存放业务数据 ==========
@dataclass
class ShopContext:
    """网店客服上下文"""
    user_id: str
    username: str
    # 模拟的订单数据
    orders: dict = field(default_factory=lambda: {
        "ORD-001": {"item": "机械键盘", "status": "已发货", "price": 399},
        "ORD-002": {"item": "无线鼠标", "status": "待发货", "price": 129},
    })

# ========== 工具:通过 context 访问业务数据 ==========
@function_tool
async def query_order(
    context: RunContextWrapper[ShopContext],
    order_id: str,
) -> str:
    """查询订单详情"""
    orders = context.context.orders
    if order_id in orders:
        order = orders[order_id]
        return (
            f"订单 {order_id}{order['item']},"
            f"价格 {order['price']} 元,状态:{order['status']}"
        )
    return f"未找到订单 {order_id}"

@function_tool
async def list_orders(
    context: RunContextWrapper[ShopContext],
) -> str:
    """列出当前用户的所有订单"""
    orders = context.context.orders
    if not orders:
        return "暂无订单"
    lines = []
    for oid, info in orders.items():
        lines.append(f"  {oid}: {info['item']} - {info['status']}")
    return f"{context.context.username} 的订单:\n" + "\n".join(lines)

# ========== Agent ==========
agent = Agent[ShopContext](
    name="网店客服",
    instructions=(
        "你是网店客服助手。可以帮用户查询订单信息。"
        "回答要简洁友好,使用中文。"
    ),
    tools=[query_order, list_orders],
    model=model,
)

# ========== 运行 ==========
async def main():
    # 上下文:当前用户的业务数据
    ctx = ShopContext(user_id="U001", username="小明")

    # Session:让 AI 记住对话历史
    session = SQLiteSession("shop-chat-001", db_path="shop.db")

    print("网店客服已上线,输入 q 退出\n")

    while True:
        user_input = input("你: ")
        if user_input.strip().lower() in ("q", "quit", "exit"):
            print("再见!")
            break

        result = await Runner.run(
            agent,
            user_input,
            context=ctx,      # 注入业务数据
            session=session,   # 保持对话记忆
        )
        print(f"客服: {result.final_output}\n")

if __name__ == "__main__":
    asyncio.run(main())

这个示例里: - RunContext 让工具函数能访问到当前用户的订单数据 - Session 让 AI 记住之前聊过的内容

两者各管各的,互相不影响。


小结

本章讲了两件事:

RunContext(依赖注入): - 定义一个 Context 类(BaseModel 或 dataclass) - 工具函数声明 RunContextWrapper[YourContext] 参数,SDK 自动注入 - Agent 用 Agent[YourContext] 泛型标注 - 运行时通过 Runner.run(..., context=ctx) 传入

Session(对话记忆): - SQLiteSession(session_id, db_path=...) 创建会话 - Runner.run(..., session=session) 自动管理对话历史 - 同一个 session_id = 同一段对话,不同 session_id = 不同对话 - 文件数据库支持跨进程、跨重启的持久化

手动历史管理: - result.to_input_list() 是旧方式,需要精细控制时仍然有用 - 日常多轮对话直接用 Session 更省事

一句话概括:Context 是给你的代码传数据,Session 是给 AI 传记忆。


下一步预告

Agent 现在既能拿到业务数据,又有了对话记忆。但目前 Agent 的回复都是自由文本,如果我们需要它输出结构化的 JSON 数据呢?

下一章我们学习结构化输出(Structured Output),让 Agent 按照你定义的格式精确输出数据。

← 第5章 流式输出 第7章 结构化输出 →