从零开始拆解Pi系列——(9)session 管理

一、引言:什么是 session

前八篇文章讲了 pi agent 的完整运行机制:调 LLM、流式事件、循环、工具、hook、skills、Extension API、compaction。这些机制都发生在"一次 agent 运行"里------agent 启动、干活、结束。但真实使用中,用户的需求不止于此:

  • agent 跑完后关掉,下次打开想接着之前的对话
  • 在某个决策点想换个方向试试,但不丢失原来的探索
  • 想回到几十轮前的某个点,从那里重新开始

这些需求需要一个东西来承载------session

session 是 pi 对"一次对话"的持久化单元。它不只是消息列表------它是一棵树,记录了对话的完整历史,包括用户消息、assistant 回复、工具结果、模型切换、工具集变更、compaction 摘要、分支摘要。每个节点是一个 entry,通过 parentId 连成父子关系。树的根是第一条消息,叶节点标记当前所在的位置。

文章 8 讲的 compaction 和 branch summarization 都是挂在 session 树上的------CompactionEntryBranchSummaryEntry 是树上的节点类型,依赖 session 树的 firstKeptEntryId / fromId 等字段工作。但 session 树本身怎么管理、怎么持久化、怎么分叉------都是黑盒。本章拆开它。

二、session 树结构

文章 8 第 5 章讲了 session 树的最小基础------entry 类型表 + getBranch + buildSessionContext。本章展开树本身的拓扑结构,以及 leaf 节点怎么控制"当前在哪个分支"。

1. 树形拓扑

session 不是链表,是树。链表只能线性追加消息,树允许在任何节点分叉出新分支:

less 复制代码
                    root (user: "帮我重构模块")
                    /                        \
          (assistant: "好的")              (assistant: "我先看看测试")
              /                                \
        (toolResult: read)              (toolResult: read test.ts)
              /                                \
        (user: "用方案 A")            (user: "用方案 B")       ← 分叉点
              /                                \
        (assistant: ...)              (assistant: ...)
              /                                \
        (assistant: "完成")           (assistant: "完成")
              ↑                        ↑
           leaf A                    leaf B

分叉的形成:用户在某个 assistant 回复后,不走"继续对话",而是通过 /fork 命令从之前的某个节点创建新分支。新分支从分叉点开始,有自己的后续消息,但分叉点之前的消息两个分支共享。

2. leaf 节点------"当前在哪"

树可以有多个分支,但 agent 同一时间只在一个分支上工作。怎么标记"当前在哪个分支"?靠 LeafEntrytypes.ts:404-407):

typescript 复制代码
interface LeafEntry extends SessionTreeEntryBase {
  type: "leaf";
  targetId: string | null;   // 指向当前叶节点(最新位置)的 entry ID
}

LeafEntry 是一种特殊的 entry------它不记录对话内容,而是记录"当前位置指针"。targetId 指向当前分支的最新 entry。

session 存储维护一个 currentLeafIdjsonl-storage.ts:168):

typescript 复制代码
class JsonlSessionStorage {
  private currentLeafId: string | null;

  async getLeafId(): Promise<string | null> {
    return this.currentLeafId;
  }

  async setLeafId(leafId: string | null): Promise<void> {
    // 在文件里追加一条 LeafEntry------记录"当前位置变了"
    const entry: LeafEntry = {
      type: "leaf",
      id: generateEntryId(this.byId),
      parentId: this.currentLeafId,
      timestamp: new Date().toISOString(),
      targetId: leafId,
    };
    await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`);
    this.currentLeafId = leafId;
  }
}

setLeafId 做两件事:

  1. 追加一条 LeafEntry 到文件 ------持久化"当前位置变了"这个事件。这样下次加载 session 时,最后一条 LeafEntry 的 targetId 就是当前分支。
  2. 更新内存里的 currentLeafId ------后续的 getBranch() / appendEntry() 都用这个值。

普通消息追加时也自动更新 currentLeafIdjsonl-storage.ts:109-111):

typescript 复制代码
function leafIdAfterEntry(entry: SessionTreeEntry): string | null {
  return entry.type === "leaf" ? entry.targetId : entry.id;
}

普通 entry 追加后,currentLeafId 变成这条 entry 的 ID------它就是最新的叶节点。只有 setLeafId(显式切换分支)才会把 currentLeafId 指向一个非最新节点。

3. getBranch------从叶到根

getPathToRootjsonl-storage.ts:275-288)从 currentLeafId 往上追溯到根,返回当前分支的完整路径:

typescript 复制代码
async getPathToRoot(leafId: string | null): Promise<SessionTreeEntry[]> {
  if (leafId === null) return [];
  const path: SessionTreeEntry[] = [];
  let current = this.byId.get(leafId);
  while (current) {
    path.unshift(current);           // 往前插------最终是根到叶的正序
    if (!current.parentId) break;
    current = this.byId.get(current.parentId);
  }
  return path;
}

从叶节点开始,沿 parentId 链一直追到根(parentId === null)。返回的数组是根到叶的正序------这就是要发给 LLM 的完整对话历史。

4. 完整树示意图

java 复制代码
entry ID    类型              parentId    内容
─────────   ──────────────    ────────    ────
a1b2        message(user)     null        "帮我重构模块"
c3d4        message(assistant) a1b2       "好的,我先读文件"
e5f6        message(toolResult) c3d4      read types.ts
g7h8        message(user)      e5f6       "用方案 A"
i9j0        message(assistant)  g7h8      "我来修改"
k1l2        message(assistant)  i9j0      "完成"
m3n4        leaf               k1l2       targetId: k1l2     ← 当前在分支 A

# 用户 /fork 从 g7h8 分叉:
o5p6        message(user)      e5f6       "用方案 B"          ← 从 e5f6 分叉
q7r8        message(assistant)  o5p6      "我换一种方式"
s9t0        message(assistant)  q7r8      "完成"
u1v2        leaf               s9t0       targetId: s9t0     ← 当前在分支 B

这棵树有两个分支,共享 a1b2 → c3d4 → e5f6 三条 entry,从 e5f6 分叉。最后一条 LeafEntryu1v2)的 targetIds9t0------当前在分支 B。getPathToRoot("s9t0") 返回 a1b2 → c3d4 → e5f6 → o5p6 → q7r8 → s9t0

如果用户执行 /tree 切回分支 A,setLeafId("k1l2") 追加一条新 LeafEntry,currentLeafId 变成 k1l2。之后 getPathToRoot("k1l2") 返回 a1b2 → c3d4 → e5f6 → g7h8 → i9j0 → k1l2

三、JSONL 持久化

session 树需要存到文件里------这样 agent 关掉再打开还能恢复。pi 用 JSONL 格式存储:每行一个 JSON 对象,第一行是 header,后续每行是一个 entry。

1. 文件格式

swift 复制代码
{"type":"session","version":3,"id":"abc123","timestamp":"2026-01-15T10:30:00Z","cwd":"/project"}
{"type":"message","id":"a1b2","parentId":null,"timestamp":"2026-01-15T10:30:01Z","message":{"role":"user","content":"帮我重构模块"}}
{"type":"message","id":"c3d4","parentId":"a1b2","timestamp":"2026-01-15T10:30:02Z","message":{"role":"assistant","content":[{"type":"text","text":"好的"}]}}
{"type":"message","id":"e5f6","parentId":"c3d4","timestamp":"2026-01-15T10:30:03Z","message":{"role":"toolResult","toolCallId":"call_1","toolName":"read","content":[{"type":"text","text":"file contents..."}]}}
{"type":"compaction","id":"g7h8","parentId":"e5f6","timestamp":"2026-01-15T10:35:00Z","summary":"## Goal\n重构模块\n## Progress\n...","firstKeptEntryId":"c3d4","tokensBefore":95000,"details":{"readFiles":["src/types.ts"],"modifiedFiles":[]}}
{"type":"leaf","id":"i9j0","parentId":"g7h8","timestamp":"2026-01-15T10:35:01Z","targetId":"g7h8"}

逐行注释:

第 1 行------headerjsonl-storage.ts:8-15):

typescript 复制代码
interface SessionHeader {
  type: "session";         // 标识这是 session 文件
  version: 3;              // 格式版本(当前 v3)
  id: string;              // session ID
  timestamp: string;       // 创建时间
  cwd: string;             // 工作目录
  parentSession?: string;  // 如果是 fork 出来的,指向父 session 文件路径
}

header 只写一次------创建 session 文件时写入,后续不再修改。parentSession 让 fork 出来的 session 能追溯来源。

第 2+ 行------entries :每行一个 SessionTreeEntry,通过 parentId 连成树。所有类型共享 SessionTreeEntryBaseid / parentId / timestamp),各自加自己的字段。

2. JsonlSessionStorage

JsonlSessionStoragejsonl-storage.ts:161-293)是 session 的存储引擎。构造时读入全部 entries 到内存:

typescript 复制代码
class JsonlSessionStorage {
  private entries: SessionTreeEntry[];           // 按追加顺序排列
  private byId: Map<string, SessionTreeEntry>;  // ID → entry 快速查找
  private labelsById: Map<string, string>;      // 标签缓存
  private currentLeafId: string | null;         // 当前叶节点

  static async open(fs, filePath): Promise<JsonlSessionStorage> {
    const loaded = await loadJsonlStorage(fs, filePath);
    return new JsonlSessionStorage(fs, filePath, loaded.header, loaded.entries, loaded.leafId);
  }
}

loadJsonlStoragejsonl-storage.ts:136-159)读文件、parse header、逐行 parse entries、跟踪 leafId

typescript 复制代码
async function loadJsonlStorage(fs, filePath) {
  const content = await fs.readTextFile(filePath);
  const lines = content.split("\n").filter(line => line.trim());
  const header = parseHeaderLine(lines[0], filePath);
  const entries = [];
  let leafId = null;
  for (let i = 1; i < lines.length; i++) {
    const entry = parseEntryLine(lines[i], filePath, i + 1);
    entries.push(entry);
    leafId = leafIdAfterEntry(entry);   // 最后一条 LeafEntry 的 targetId,或最后一条普通 entry 的 id
  }
  return { header, entries, leafId };
}

加载时 leafId 不断被后面的 entry 覆盖------最终值就是"最后一条 entry 标记的当前位置"。如果是 LeafEntry,取 targetId;如果是普通 entry,取 id

3. appendEntry------追加写入

appendEntryjsonl-storage.ts:250-259)是 session 写入的唯一入口------每条新消息都调它:

typescript 复制代码
async appendEntry(entry: SessionTreeEntry): Promise<void> {
  await this.fs.appendFile(this.filePath, `${JSON.stringify(entry)}\n`);
  this.entries.push(entry);
  this.byId.set(entry.id, entry);
  updateLabelCache(this.labelsById, entry);
  this.currentLeafId = leafIdAfterEntry(entry);
}

关键设计------只追加,不修改。session 文件从不重写------新 entry 追加到文件末尾。这让写入操作是 O(1) 的(不需要重写整个文件),且不会因崩溃损坏已有内容。

setLeafId 也是追加(jsonl-storage.ts:237-238)------追加一条 LeafEntry,不修改之前的任何行。即使切回旧分支,也只是追加一条新的 leaf 指针,旧的 leaf 指针留在文件里记录历史。

4. create------创建新 session

createjsonl-storage.ts:191-213)创建新 session 文件:

typescript 复制代码
static async create(fs, filePath, options: { cwd, sessionId, parentSessionPath? }) {
  const header: SessionHeader = {
    type: "session",
    version: 3,
    id: options.sessionId,
    timestamp: new Date().toISOString(),
    cwd: options.cwd,
    parentSession: options.parentSessionPath,
  };
  await fs.writeFile(filePath, `${JSON.stringify(header)}\n`);  // 写 header + 换行
  return new JsonlSessionStorage(fs, filePath, header, [], null);  // 空 entries,null leafId
}

创建时只写 header------一个空 session 文件就是一行 JSON。后续所有 entry 通过 appendEntry 追加。

四、分支操作

session 树支持四种操作:创建新 session、从已有 session 分叉、切换到另一个 session、在当前 session 树内切换分支。

操作示意图

less 复制代码
Session 文件 A                          Session 文件 B(fork from A 的 e5f6)
┌──────────────────────┐                ┌──────────────────────┐
│ header (id: abc123)  │                │ header (id: def456)  │
│ a1b2 user            │──── fork ────→│ parentSession: A     │
│ c3d4 assistant       │   从 e5f6      │ a1b2 user (复制)     │
│ e5f6 toolResult      │                │ c3d4 assistant (复制) │
│ g7h8 user            │                │ e5f6 toolResult(复制)│
│ i9j0 assistant       │                │ o5p6 user (新)       │
│ leaf: i9j0           │                │ q7r8 assistant (新)  │
└──────────────────────┘                │ leaf: q7r8           │
                                        └──────────────────────┘

Session 文件 A 内部(navigateTree):
┌──────────────────────┐
│ ...                  │
│ g7h8 user            │  ← 切到这里(setLeafId g7h8)
│ i9j0 assistant        │
│ leaf: i9j0           │  ← 之前在这里
│                      │
│ leaf: g7h8 (新追加)  │  ← setLeafId 追加的新 LeafEntry
└──────────────────────┘

1. newSession------创建全新 session

伪代码(session-manager.ts:824-849):

kotlin 复制代码
function newSession(options?):
    sessionId = options.id ?? generateSessionId()
    header = {
        type: "session",
        version: 3,
        id: sessionId,
        timestamp: now(),
        cwd: this.cwd,
        parentSession: options.parentSession,    # 有父 session 才填
    }
    this.fileEntries = [header]     # 只剩 header
    this.byId.clear()
    this.leafId = null              # 空 session,没有叶节点
    this.sessionFile = "{timestamp}_{sessionId}.jsonl"
    return this.sessionFile

创建全新 session------清空所有状态,写一个新 header 到新文件。后续消息从这个空状态开始追加。如果 parentSession 有值,说明这个 session 是从另一个 session fork 出来的------header 记录溯源。

2. forkFrom------从已有 session 分叉

伪代码(session-manager.ts:1434-1485):

ini 复制代码
static function forkFrom(sourcePath, targetCwd, options?):
    # 1. 读源 session 文件
    sourceEntries = loadEntriesFromFile(sourcePath)
    sourceHeader = sourceEntries.find(e => e.type == "session")

    # 2. 创建新 session 文件
    newSessionId = generateSessionId()
    newSessionFile = "{timestamp}_{newSessionId}.jsonl"
    newHeader = {
        type: "session",
        version: 3,
        id: newSessionId,
        timestamp: now(),
        cwd: targetCwd,
        parentSession: sourcePath,     # 指向源文件------溯源
    }
    writeFile(newSessionFile, newHeader)

    # 3. 复制源 session 的所有 entry(不含 header)
    for entry in sourceEntries:
        if entry.type != "session":
            appendFile(newSessionFile, entry)

    # 4. 返回新的 SessionManager 实例
    return new SessionManager(targetCwd, dir, newSessionFile)

forkFrom 做三件事:

  1. 读源文件:把源 session 的所有 entry 读出来
  2. 写新 header :新 ID、新时间戳、parentSession 指向源文件路径------这让 fork 出来的 session 能追溯来源
  3. 复制 entries:把源文件的所有 entry(不含 header)追加到新文件------全部复制,包括 LeafEntry

关键设计------fork 是文件级别的复制,不是树内部的分叉 。fork 创建一个全新的 JSONL 文件,把源文件的内容全部复制过来。新 session 和旧 session 是两个独立文件,各自演化。新 session 的 parentSession 字段记录溯源关系。

为什么全部复制而不是引用?因为 session 文件是只追加的------引用需要处理"源文件后续新增 entry 怎么办"。全复制更简单------fork 出来的 session 独立演化,不受源文件影响。

3. setSessionFile------切换到另一个 session

伪代码(session-manager.ts:792-822):

kotlin 复制代码
function setSessionFile(sessionFile):
    this.sessionFile = sessionFile
    if exists(sessionFile):
        this.fileEntries = loadEntriesFromFile(sessionFile)
        if this.fileEntries.length == 0:
            # 文件空或损坏------重新初始化
            this.newSession()
            this.sessionFile = sessionFile
            this._rewriteFile()
            return

        header = this.fileEntries.find(e => e.type == "session")
        this.sessionId = header.id

        # 如果需要迁移旧版本格式
        if migrateToCurrentVersion(this.fileEntries):
            this._rewriteFile()

        this._buildIndex()       # 重建 byId Map + leafId
        this.flushed = true
    else:
        # 文件不存在------新建
        this.newSession()
        this.sessionFile = sessionFile

setSessionFile 是 session 级别的切换------从当前 session 文件切到另一个 session 文件。加载新文件的所有 entry,重建内存索引(byId Map + leafId),后续从新文件的当前位置继续。

_buildIndexsession-manager.ts:851-870)遍历所有 entry 重建 byId Map 和 leafId------和 loadJsonlStorage 一样的逻辑,但这是 SessionManager 自己的实现(coding-agent 层有自己的 session 存储逻辑,不完全依赖 agent 层的 JsonlSessionStorage)。

navigateTree 不是一个单独的函数------它是通过 setLeafId 实现的。用户用 /tree 命令选择一个 entry,pi 调 session.setLeafId(targetEntryId),追加一条新 LeafEntrycurrentLeafId 指向选中的 entry。

bash 复制代码
function navigateTree(targetEntryId):
    session.setLeafId(targetEntryId)
    # setLeafId 内部:
    #   1. 追加 LeafEntry { targetId: targetEntryId }
    #   2. 更新 currentLeafId = targetEntryId
    # 之后 getBranch() 返回新路径

setSessionFile 的区别:

setSessionFile navigateTree (setLeafId)
切换范围 跨文件 同一文件内
操作 加载新文件 + 重建索引 追加 LeafEntry + 更新 leafId
持久化 新文件的内容全部加载 只追加一行 LeafEntry

四种操作对比

操作 文件级别 做什么 触发命令
newSession 创建新文件 清空状态,写新 header /new
forkFrom 创建新文件 复制源文件内容 + 新 header 指向源 /fork
setSessionFile 切换文件 加载目标文件 + 重建索引 /resume
setLeafId 同一文件 追加 LeafEntry + 更新 leafId /tree

五、session 恢复

agent 启动时需要恢复之前的 session------读 JSONL 文件、重建内存索引、恢复 context。

1. 启动加载流程

scss 复制代码
pi 启动
  → 检查 session 目录(~/.pi/agent/sessions/<encoded-cwd>/)
  → 有 session 文件?→ 加载最新的 / 用户指定的
  → JsonlSessionStorage.open(filePath)
    → loadJsonlStorage()
      → 读文件全部内容
      → parse header(第一行)
      → 逐行 parse entries
      → 跟踪 leafId(最后一条 entry 标记的位置)
    → 构建 byId Map + labelsById Map + currentLeafId
  → buildSessionContext(pathEntries)
    → 从路径重建 messages 数组(文章 8 第 5 章讲过)
  → 恢复 model / thinkingLevel / activeTools(从 path 上的状态变更 entry 读取)
  → 准备好,等待用户输入

2. loadJsonlStorage------读文件重建内存

loadJsonlStoragejsonl-storage.ts:136-159)在第 3 章讲过------读文件、parse header、逐行 parse entries、跟踪 leafId。加载完成后,JsonlSessionStorage 的内存状态:

yaml 复制代码
entries: [header, entry1, entry2, ..., entryN]   # 按文件顺序
byId: { "a1b2": entry1, "c3d4": entry2, ... }     # ID → entry 快速查找
labelsById: { "a1b2": "重要节点" }                 # 标签缓存
currentLeafId: "s9t0"                              # 最后一条 entry 标记的位置

byId Map 是关键------getPathToRoot 用它做 parentId 链追溯。没有它就要每次遍历 entries 数组,O(n) 变 O(1)。

3. buildSessionContext------从路径重建 context

文章 8 第 5 章讲过 buildSessionContextsession.ts:22-80)。它在启动恢复时被调:

typescript 复制代码
// session.ts:109-115
async getBranch(fromId?: string): Promise<SessionTreeEntry[]> {
  const leafId = fromId ?? await this.getLeafId();
  return this.storage.getPathToRoot(leafId);
}

async buildContext(): Promise<SessionContext> {
  return buildSessionContext(await this.getBranch());
}
  1. getBranch()currentLeafId 往上追溯到根,返回路径上所有 entry
  2. buildSessionContext() 遍历路径,处理 compaction entry(用摘要替换旧消息),恢复 model / thinkingLevel / activeToolNames

恢复后的 context 和上次关闭时一致------LLM 下一轮看到的是"上次最后一条消息 + 之前的完整历史(或 compaction 摘要 + 保留消息)"。

4. 版本迁移

session 文件格式可能升级(当前 v3)。setSessionFilesession-manager.ts:811-813)加载时检查版本:

typescript 复制代码
if (migrateToCurrentVersion(this.fileEntries)) {
  this._rewriteFile();   # 迁移后重写整个文件
}

如果文件是旧版本,migrateToCurrentVersion 把旧格式 entry 转成新格式,然后 _rewriteFile 重写整个文件。这是 session 管理里唯一需要重写文件的场景------正常操作都是只追加。

5. 恢复后的状态

恢复完成后的状态:

状态 来源
messages 数组 buildSessionContext 从 path 重建(含 compaction 摘要 + 保留消息)
model / provider 路径上最后一条 model_change entry 或最后一条 assistant 消息
thinkingLevel 路径上最后一条 thinking_level_change entry
activeToolNames 路径上最后一条 active_tools_change entry
currentLeafId 文件最后一条 entry 标记的位置

agent 从这个状态继续------用户可以接着输入,也可以用 /tree 切分支、/resume 切 session、/fork 分叉新探索。

六、Q&A

Q1:session 文件会无限增长吗?

会,但有 compaction 控制。session 文件是只追加的------每条消息、每次状态变更都追加一行。长对话的文件可能到几 MB。

compaction 不删除旧 entry------它追加一条 CompactionEntry 标记"从这里开始用摘要"。旧的 message entry 仍在文件里,但 buildSessionContext 重建 context 时跳过它们。文件变大了,但 context 不变。

如果文件确实太大,可以 /fork 创建新 session------只复制当前路径上的 entry(compaction 后的近期消息 + 摘要),旧文件留着不删。

Q2:session 文件损坏了怎么办?

loadJsonlStorage 逐行 parse------某行 JSON 损坏只跳过那一行,不影响其他 entry。parseEntryLine 对每行做校验(type / id / parentId / timestamp),损坏的行记 warning 跳过。

如果 header 损坏(第一行不是有效 session header),整个文件不可用------setSessionFile 检测到后 newSession() 重新初始化该文件。

_rewriteFile 是另一个恢复路径------版本迁移时重写整个文件。如果迁移过程中崩溃,原文件可能损坏。pi 用同步写入(writeFileSync),减少部分写入的风险。

Q3:可以在不同项目间共享 session 吗?

可以------session 文件是独立的 JSONL 文件。/import 命令导入外部 session 文件,/export 导出。

但 session 的 header 里有 cwd------加载时如果 cwd 不匹配当前工作目录,工具的文件路径可能不对(如 read 的路径是相对于旧 cwd 的)。fork 时可以指定新的 targetCwd,header 更新成新目录。

Q4:fork 出来的 session 修改了,源 session 会受影响吗?

不会。fork 是文件级别的全复制------新 session 是完全独立的文件。源 session 后续追加的 entry 不会出现在 fork 出来的 session 里,反过来也一样。

唯一保留的关联是 parentSession 字段------fork 出来的 session 的 header 记录源文件路径。这只用于溯源展示(如 UI 显示"forked from xxx"),不影响功能。

七、下一章预告

下一篇文章将进入 pi 的 slash 命令系统------用户输入 /xxx 时 pi 怎么解析、命令的三层来源(内置 / Extension / prompt template)、自动补全机制、命令 handler 的执行流程。本章第 4 章的 /fork / /tree / /resume / /new 都是内置 slash 命令,下一篇拆解它们的注册、解析和执行机制。

相关推荐
韦东东32 分钟前
规则 or Agent 驱动:一个合同审查系统的产品设计与工程拆解
pipeline·agent·合同审查·模型与规则
GreatVicent2 小时前
AI Agent互操作标准化加速:A2A协议正式加入Linux基金会
linux·运维·人工智能·agent·aws·a2a
寻道码路2 小时前
大模型工程化实战(七):JSON Schema 强约束——模型吐的 json 不合规?schema 校验不通过,重试、兜底、降级一条龙
大模型·agent·rag·json schema·ai工程化·llmops`
Csvn2 小时前
第 14 章 工具增强 Tool Augmentation
人工智能·aigc·agent
七牛云行业应用2 小时前
DSH Desktop 桌面版实测:Win/macOS 一键安装,“桌面也是插件“怎么理解,和 npx 起 Web UI 差在哪
人工智能·agent·ai编程
狂师2 小时前
AI 测试提效 | 别搞万能 Skill,推荐5 个 Agent Skill 串起 UI 自动化执行到报告生成全流程
人工智能·agent·测试
张彦峰ZYF3 小时前
从对话记忆到状态控制平面:长程 Agent 的状态治理工程
人工智能·llm·agent·loopengineering·langgroup
Steve__evetS3 小时前
我的开源项目:python依赖安全修复助手
python·安全·agent
Steve__evetS3 小时前
Claude Code记忆机制
agent·记忆·claude code