Skip to content

第 5 章 · 会话与上下文管理

本章目标:理解 Flue 的会话模型与 ID 寻址机制,掌握跨对话上下文保持的原理,学会设计合理的会话生命周期。

5.1 会话是 Agent 的"记忆单元"

没有会话的 LLM 调用是失忆的:每条消息都是全新开始。Flue 中每个 agent 实例都绑定一个会话(session),会话按 ID 持久化:

text
Agent 函数(定义)          会话实例(运行时状态)
┌─────────────────┐       ┌──────────────────────┐
│ export function │       │ id: "ticket-1024"     │
│ Triage() {...}  │ ────▶ │ messages: [...历史]    │
│                 │  按 ID │ state: 持久标志位      │
└─────────────────┘  实例化 └──────────────────────┘
  • Agent 定义是无状态的函数模板,全局一份;
  • 会话实例是有状态的运行时对象,每个 ID 一份;
  • 同一个 agent 可以同时服务成千上万个会话而互不干扰。

5.2 用 ID 建立多轮对话

ID 的语义完全由你的业务决定——用户 ID、工单号、GitHub issue 编号、随机字符串皆可:

bash
# CLI 方式:--id 创建/续接一个具名会话
npx flue run src/agents/assistant.ts --id crab-owner-42 --message \
  "我想养一只宠物蟹,帮我想个名字"
npx flue run src/agents/assistant.ts --id crab-owner-42 --message \
  "就选你说的第一个名字,给我三个它的昵称"
# 第二条消息能接住第一条的话题——因为同 ID 共享消息历史
typescript
// HTTP 方式:POST /:id 中的 :id 就是会话标识
// src/app.ts 挂载路由后:
//
// curl -X POST http://localhost:5173/agents/support/user-8817 \
//   -H 'content-type: application/json' \
//   -d '{"text": "我的订单还没到"}'
//
// user-8817 这个用户的后续消息都会落在同一个会话里

ID 设计建议

把 ID 当作业务主键用:客服场景用 user-{uid}, 运维场景用 {repo}-{issue}。这样天然实现"一人一会话、一事一会话",且重启后凭 ID 即可恢复。

5.3 上下文的组成与自动压缩

发给模型的完整上下文由三部分组成,其中会话历史会随对话增长:

typescript
'use agent';
import { useModel } from '@flue/runtime';

export function LongTaskAgent() {
  useModel('anthropic/claude-sonnet-4-6', {
    // 长任务必配:接近上限时 Flue 自动压缩历史
    compaction: {
      reserveTokens: 30000,     // 提前触发:已用 token 超过 上限-30000 时压缩
      keepRecentTokens: 16000,  // 最近 16k token 的消息保持原文
      model: 'anthropic/claude-haiku-4-5', // 摘要工作交给便宜模型
    },
  });
  return `
执行一次完整的代码库安全审计:
逐目录扫描、记录发现、汇总报告。
任务很长,请边做边在回复中维护"当前进度"小结。`;
}

压缩机制的工作方式:旧历史折叠成摘要 + 最近消息保留原文,会话不中断地继续。这让单个会话可以跑得比任何模型的上下文窗口都长——这正是第 1 章说的 Durability 特性的一部分。

5.4 跨轮持久化的自定义状态

消息历史之外,agent 还有自己的持久状态槽位——usePersistentState

typescript
'use agent';
import { useModel, usePersistentState, useTool } from '@flue/runtime';
import { defineTool } from '@flue/runtime';
import * as v from 'valibot';

// 一个让模型主动登记结论的工具,写入持久状态
const recordFinding = defineTool({
  name: 'record_finding',
  description: '把一条审计发现记入会话级清单。',
  input: v.object({ severity: v.string(), detail: v.string() }),
  async run() { return { output: '已记录' }; }, // 简化示例
});

export function Auditor() {
  useModel('anthropic/claude-sonnet-4-6');
  useTool(recordFinding);

  // findings 在整个会话生命周期内持久保存
  const [findings] = usePersistentState<string[]>('findings', []);

  return `
你是安全审计员。
${findings.length > 0
  ? `已有 ${findings.length} 条记录,继续扫描未覆盖的目录。`
  : '开始第一轮全库扫描。'}
每完成一项检查就用 record_finding 登记。`;
}

注意与第 3 章"重渲染"的结合:findings.length 变了,下一轮指令就跟着变——这就是"指令反映当前状态"的实际用法。

5.5 会话生命周期设计

场景推荐 ID 策略生命周期
个人助手user-{uid}长期,跟随用户
工单处理ticket-{no}工单关闭即归档
CI 任务run-{pipeline_id}单次流水线
定时巡检patrol-{date}按天滚动

两条实践建议:

  1. 宁可会话粒度细:一个大杂烩会话会让旧上下文稀释新任务的注意力;无关任务开新会话;
  2. 依赖压缩而非手动清理:与其自己截断历史,不如配置好 compaction 让框架处理。
typescript
// 会话 ID 的类型化封装建议——把 ID 语义写进类型里
// 避免各处手拼字符串导致格式漂移
type SessionId<T extends string> = `${T}-${string}`;

const userSession = (uid: string) => `user-${uid}` as const;      // user-8817
const ticketSession = (no: number) => `ticket-${no}` as const;    // ticket-1024
const runSession = (pid: string) => `run-${pid}` as const;        // run-ci-7788

// 统一出口:所有业务模块都从这里拿会话 ID
export const sessions = { userSession, ticketSession, runSession };

本章小结

  • Agent 定义无状态,会话实例有状态,靠 ID 关联;
  • ID 语义业务自定义,CLI 用 --id、HTTP 用 POST /:id
  • 长会话靠 compaction 自动压缩:摘要旧历史 + 保留近期原文;
  • usePersistentState 提供消息之外的业务状态持久化;
  • 会话粒度宜细不宜粗,压缩交给框架。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. Flue 中会话实例和 Agent 函数的关系是?

2. HTTP 方式向指定会话发消息的路由是?

3. compaction 触发时会发生什么?

4. 关于会话 ID 的设计,下列做法最合理的是?

🛠️ 动手实践

  1. 为同一个 agent 开两个不同 ID 的会话,验证它们的消息互不可见。
  2. 给长任务 agent 配置 compaction 并故意跑超长任务,观察摘要替换原文的行为。
  3. usePersistentState 实现"已询问过的问题列表",让 agent 避免重复提问。

会话有了记忆。但光会说话不够——下一章让 Agent动手调用你的代码