Session Assistant工具链三节点设计详解

2026-06-04阅读 0热度 0
session

1. 为什么需要三节点

在Agent设计理论中,有个经典模式几乎被所有主流方案默认采用: 第二回: Session Assistant 工具链的三节点设计 用户输入 → Thought(想做什么)→ Action(执行工具)→ Observation(看到什么)→ 再思考 or 直接回答 核心思想其实就一句话:把「决策」「执行」「感知结果」拆开,别让大模型一边编故事一边当事实输出。否则,凭空捏造的幻觉就成了常态。 TextFlow 会话 Assistant 的工具链走的就是这个路子,只不过做了有意的简化,当前版本用的是下面这张对照表上的那一套: | 理论上的Agent | 当前实现 | |---|---| | Thought 由 LLM 推理 | Thought 由规则引擎完成 | | Action 可调多种工具 | Action 只有一种能力:查 SQLite | | Observation 后可能多轮循环 | 固定一轮,Observation 后直接交给 LLM 生成回复 | | Observation 进入对话历史 | Observation 只进 system prompt,用户不可见 | 理解这套取舍,是读懂所有代码的前提。说穿了,先知道「本来应该是什么样」,再看「实际上做了什么简化」,才不会被代码迷惑。

2. 工具链在整体流程中的位置

Assistant 模式下,用户发一条消息,等待的其实是串行链: sendMessage()
├─ [Assistant] runToolLoop() ← 工具链(同步,不调 LLM)
├─ buildSessionSystemPrompt() ← 把 observation 拼进 system
└─ streamSessionChat() ← LLM 流式回复
关键特征是串行:先查库,再生成回答。对话模式直接跳过工具链。 编排入口几乎可以用「三个节点,无循环」来概括——极其简洁: // src/agents/sessionAssistant/toolLoop.ts(节选)
state = { ...state, ...(await thoughtNode(state)) };
if (!state.shouldAct) return state;
state = { ...state, ...(await actionNode(state)) };
state = { ...state, ...observationNode(state) };
return state;
没有 while 循环,没有递归出口的判断,干净利落。

3. 共享状态:AgentGraphState

三节点之间靠同一个状态对象传递信息,这是 Graph / State Machine 思想的体现: // src/agents/sessionAssistant/schema.ts(节选)
interface AgentGraphState {
userMessage: string; // 原始输入
thought: string | null; // Thought 产出:给人看的说明
shouldAct: boolean; // 是否进入 Action
queryRequest: AssistantQueryRequest | null; // Thought 产出:查什么
dbContext: AssistantContextResult | null; // Action 产出:结构化数据
errorMessage: string | null;
observation: string | null; // Observation 产出:给 LLM 的 Markdown
phase: "idle" | "thought" | "action" | "observation" | "done";
}
设计要点很明确:每个节点只返回 Partial,由编排器 merge。节点之间不直接调用,方便单独替换或测试。这在实际工程中很有用——比如哪天要把规则引擎换成 LLM,只需要替换 thoughtNode 这一个函数。

4. 节点一:Thought ——「要不要查、查什么」

4.1 理论职责

理论上,Thought 要回答两个核心问题:第一,是否需要借助外部世界(这里是数据库);第二,如果需要,具体执行哪类操作。 在完整 Agent 里,这是 LLM 的输出环节;但在这里,是确定性规则。

4.2 实现本质

Thought 节点本质上只做两件事: // src/agents/sessionAssistant/nodes.ts — thoughtNode
const queryRequest = resolveAssistantQuery(state.userMessage);
if (!queryRequest) {
return { shouldAct: false, thought: "未触发数据库查询,走普通对话。" };
}
return {
shouldAct: true,
queryRequest,
thought: `检测到数据类问题:${describeAssistantQuery(queryRequest)}。`,
};
resolveAssistantQuery() 是核心路由逻辑,分两个层次: 第一层是门控:消息是否像「数据类问题」? // 命中任一正则即视为数据问题
DATA_QUERY_PATTERNS.some((p) => p.test(message))
// 例:/项目/、/剧本/、/进度/、/几个/ …
第二层是路由:决定查询类型和参数。 | 用户意图信号 | queryRequest.kind | |---|---| | 「第 3 集剧本」 | `script_episode` + `episodeIndex: 3` | | 「剧本内容 / 列表」 | `script_list` | | 「故事骨架写了什么」 | `story_skeleton` | | 「改编策略」 | `adaptation_strategy` | | 「几个项目 / 项目列表」 | `project_list` | | 「进度 / 工作流 / 节点状态」 | `project_detail` | | 其它数据类问题 | 默认 `project_list` |

4.3 和理论的对照

| | 理论 Thought | 当前 Thought | |---|---|---| | 本质 | 推理、规划 | 正则分类 + if/else 路由 | | 输出 | 自然语言推理链 | `shouldAct` + 结构化 `queryRequest` | | 字段 | 模型内心独白 | UI 展示文案,不参与决策 | 实战结论:名字叫 Thought,实现是 Intent Router。优点是零成本、可预测;代价是没法理解同义表达——比如「帮我看看进展」可能就不触发。

5. 节点二:Action ——「去外部世界拿事实」

5.1 理论职责

Action 是 Agent 与环境的接触点。环境在这里不是互联网,是桌面应用内的 SQLite 业务库。 必须强调一条原则:LLM 不直接碰数据库,用受控通道代查,保证数据真实、权限可控。

5.2 实现

Action 节点根据 Thought 给出的 queryRequest,调用 Tauri 命令: // src/agents/sessionAssistant/nodes.ts — actionNode
const result = await invoke("query_assistant_context", {
input: {
queryKind: state.queryRequest.kind,
episodeIndex: state.queryRequest.episodeIndex ?? null,
userMessage: state.userMessage, // 用于解析「哪个项目」
},
});
return { dbContext: result, phase: "action" };
Rust 端(src-tauri/src/assistant_context.rs)按 queryKind 分支,返回不同粒度的数据: | kind | 返回内容 | |---|---| | `project_list` | 所有项目 + 简要统计 | | `project_detail` | 单项目 + 六节点工作流状态 | | `script_list` | 剧本目录(预览,非全文) | | `script_episode` | 指定集正文(可截断) | | `story_skeleton` | 故事骨架全文 | | `adaptation_strategy` | 改编策略全文 | 设计原则体现得比较清晰:最小必要数据——问列表就不拉剧本全文,控制 token 与溢出风险;单一入口——前端只认一个 IPC 命令,Rust 内部路由,避免工具膨胀;失败可捕获——异常写入 errorMessage,不会抛到 UI 层崩溃。

5.3 和理论的对照

Action 在 ReAct 里对应 Tool Use 的执行阶段。当前是 1 个工具、N 种查询模式,还没到 Function Calling 那种「模型自选工具」的层面。

6. 节点三:Observation ——「把事实变成 LLM 能读的东西」

6.1 理论职责

Observation 是 Action 之后 Agent「看到」的结果。 在循环型 Agent 里,Observation 会回到 Thought,驱动下一步。但当前实现里,Observation 的终点只有一个:构造一段文本,让 LLM 在生成答案时有据可依。

6.2 实现

Observation 不做查询、不做推理,只做格式化加使用说明: // src/agents/sessionAssistant/nodes.ts — observationNode(逻辑摘要)
return {
observation: [
`**数据库查询结果** — ${description}`,
introByKind[queryKind], // 告诉 LLM 该怎么用这份 JSON
"```json",
JSON.stringify(result, null, 2),
"```",
].join("\n"),
};
不同 queryKind 附带不同 introByKind,比如 script_list 会附带「以下为目录预览,不含全文;若问某一集请说明集数」,script_episode 则是「请基于 script.content 回答,不要编造」。

6.3 Observation 体现在哪里?

| 位置 | 是否包含 observation | |---|---| | 用户聊天气泡 | 否(用户只看到 LLM 的自然语言回答) | | tool-status 进度条 | 否(只显示「整理查询结果…」) | | LLM system prompt | 是 ← Observation 的唯一生效处 | | 多轮 history | 否 | 注入方式: // src/agents/sessionAssistant/textflowChatAgent.ts
function buildSessionSystemPrompt(agentObservation?) {
const parts = [角色 Prompt, 产品 Skill];
if (agentObservation) {
parts.push(`## 本地数据库查询结果\n${agentObservation}`);
}
return parts.join("\n\n");
}
实战结论:Observation = 给模型的「结构化上下文补丁」,不是给用户看的中间产物。

7. 三节点串起来:一次完整请求

以用户输入「剧本内容」为例,走一遍全流程: ┌─────────────────────────────────────────────────────────┐
│ Thought │
│ resolveAssistantQuery("剧本内容") │
│ → 命中 /剧本/ + /内容/ → kind: script_list │
│ → shouldAct: true │
│ → thought: "检测到数据类问题:查询项目剧本列表…" │
└──────────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ Action │
│ invoke("query_assistant_context", { script_list, … }) │
│ → Rust: 定位项目 → fetch_scripts → 转 brief + preview │
│ → dbContext: { queryKind, description, scripts: [...] } │
└──────────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ Observation │
│ formatObservationPayload(dbContext) │
│ → Markdown + JSON + 「以下为目录预览…」 │
│ → observation: string │
└──────────────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ LLM(工具链之外) │
│ system = 角色 + Skill + observation │
│ → 流式生成表格化回答 │
└─────────────────────────────────────────────────────────┘
如果用户问的是「你好」呢?流程更干脆:Thought → resolveAssistantQuery 返回 null → shouldAct: false → 早退(没有 Action、没有 Observation,LLM 只带产品 Skill 回答)。

8. 与 Agent 设计原则的对照表

| 设计原则 | 理论期望 | 当前实战 | |---|---|---| | Grounding(接地) | 回答基于真实数据 | ✅ Observation 注入 DB 结果;Skill 要求不编造 | | Separation of concerns | 决策/执行/感知分离 | ✅ 三节点 + 独立状态字段 | | Controlled tools | 工具白名单、受控通道 | ✅ 单一 Tauri 命令 + Rust 路由 | | Minimal context | 按需取数 | ✅ 按 queryKind 分级返回 | | Reasoning in Thought | LLM 规划 | ❌ 规则引擎代替 | | Multi-step loop | 查→看→再查 | ❌ 固定一轮 | | Transparent trace | 可审计推理链 | ⚠️ 仅有 tool-status 摘要,observation 对用户不可见 |

9. 演进方向(理论指导下的下一步)

如果要继续往「完整 Agent」靠拢,三节点分别有明确的升级路径。 Thought 可以走真推理——用 LLM + Function Calling 替代正则路由,或者 hybrid 方案:规则做门控,模型做细分类。 Action 可以拓展成多工具——把 list_projectsget_script 等拆成独立 tool,由模型选择调用哪个。 Observation 可以进入对话协议——用 role: tool message 写入 history,而不是只塞进 system。支持 Observation 触发第二轮 Thought:比如查列表后发现用户问第 2 集,再查正文。 至于循环,runToolLoop 加 while + maxSteps 就能实现 ReAct 闭环。

10. 关键文件索引

| 环节 | 文件 | |---|---| | 编排器 | `src/agents/sessionAssistant/toolLoop.ts` | | 三节点 | `src/agents/sessionAssistant/nodes.ts` | | 意图路由 / 状态类型 | `src/agents/sessionAssistant/schema.ts` | | Observation 注入 LLM | `src/agents/sessionAssistant/textflowChatAgent.ts` | | 发送调度 | `src/hooks/useAppState.ts` | | DB 查询后端 | `src-tauri/src/assistant_context.rs` |

11. 总结

Session Assistant 工具链是 ReAct 三节点思想的轻量落地:Thought 是规则路由,输出 shouldAct + queryRequest;Action 走 Tauri 查 SQLite,输出结构化 dbContext;Observation 格式化为 Markdown + JSON,注入 system prompt。 它验证了设计原则里最重要的一条:先拿事实,再让 LLM 说话。同时也诚实暴露了简化版的边界——Thought 不是真思考,Observation 用户看不见,没有多轮工具循环。 读懂这三节点各自「名义上是什么」和「代码里实际是什么」,比记住文件名更有用。这也是理论文章走通到实战时最该对照的地方。
免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策