进阶教程
综合资讯
Session Assistant工具链三节点设计详解
摘要
TextFlow会话助手工具链采用三节点串行设计:Thought节点通过规则引擎判断是否查询数据库并
1. 为什么需要三节点
在Agent设计理论中,有个经典模式几乎被所有主流方案默认采用:
用户输入 → 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_projects、get_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 用户看不见,没有多轮工具循环。
读懂这三节点各自「名义上是什么」和「代码里实际是什么」,比记住文件名更有用。这也是理论文章走通到实战时最该对照的地方。 来源:互联网
免责声明
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。