第二回: Session Assistant 工具链的三节点设计

本文是对 ReAct / Thought-Action-Observation 设计原则的一次落地复盘

结合 Isshin AI TextFlow 会话模块中的工具链实现,说明「思考 → 行动 → 观察」在真实产品里各自承担什么、边界在哪里,以及和「完整 Agent」的差距。


1. 为什么需要三节点

在 Agent 设计理论里,一个常见模式是:

复制代码
用户输入 → Thought(想做什么)→ Action(执行工具)→ Observation(看到什么)→ 再思考 or 直接回答

核心思想:把「决策」和「执行」和「感知结果」拆开,避免 LLM 凭空编造事实。

TextFlow 会话 Assistant 的工具链走的就是这条路径,但做了有意简化

理论上的 Agent 当前实现
Thought 由 LLM 推理 Thought 由规则引擎完成
Action 可调多种工具 Action 只有一种能力:查 SQLite
Observation 后可能多轮循环 固定一轮,Observation 后直接交给 LLM 生成回复
Observation 进入对话历史 Observation 只进 system prompt,用户不可见

理解这套取舍,是读懂代码的前提。


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

Assistant 模式下,用户发送一条消息后:

scss 复制代码
sendMessage()
  ├─ [Assistant] runToolLoop()     ← 工具链(同步,不调 LLM)
  ├─ buildSessionSystemPrompt()    ← 把 observation 拼进 system
  └─ streamSessionChat()           ← LLM 流式回复

工具链与 LLM 串行 :先查完库,再生成回答。

「对话」模式完全跳过工具链。

编排入口极简------一个状态对象,三个节点,无循环:

typescript 复制代码
// 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;

3. 共享状态:AgentGraphState

三节点通过同一个状态对象传递信息,这是 Graph / State Machine 思想的体现:

typescript 复制代码
// 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<State>,由编排器 merge。节点之间不直接调用,便于单独替换或测试。


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

4.1 理论职责

Thought 回答两个问题:

  1. 是否需要借助外部世界(这里是数据库)?
  2. 若需要,具体执行哪类操作

在完整 Agent 里,这通常是 LLM 的输出;在我们这里,是确定性规则

4.2 实现本质

Thought 节点只做两件事:

typescript 复制代码
// 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() 是核心路由逻辑,分两层:

第一层 --- 门控: 消息是否像「数据类问题」?

typescript 复制代码
// 命中任一正则即视为数据问题
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
thought 字段 模型内心独白 UI 展示文案,不参与决策

实战结论: 名字叫 Thought,实现是 Intent Router。优点是零成本、可预测;代价是无法理解同义表达(「帮我看看进展」可能不触发)。


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

5.1 理论职责

Action 是 Agent 与环境 的接触点。环境在这里不是互联网,而是桌面应用内的 SQLite 业务库

原则:LLM 不直接碰数据库,由受控通道代查,保证数据真实、权限可控。

5.2 实现

Action 节点根据 Thought 给出的 queryRequest,调用 Tauri 命令:

typescript 复制代码
// 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 不做查询、不做推理,只做格式化 + 使用说明

typescript 复制代码
// 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

注入方式:

typescript 复制代码
// 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. 三节点串起来:一次完整请求

以用户输入 「剧本内容」 为例:

typescript 复制代码
┌─────────────────────────────────────────────────────────┐
│ 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                     │
│  → 流式生成表格化回答                                     │
└─────────────────────────────────────────────────────────┘

若用户问 「你好」

yaml 复制代码
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 用户看不见,且没有多轮工具循环。

读懂这三节点各自「名义上是什么」和「代码里实际是什么」,比记住文件名更有用------这也是理论文章走通到实战时最该对照的地方。

相关推荐
云间寄信1 小时前
异步编程与事件循环
javascript
猎奇不再看1 小时前
深度解析 Java 双向链表的性能与源码真相
javascript
weiwin1231 小时前
入门(4):使用 MAF Middleware 中间件
agent
阿里云云原生2 小时前
从追踪到治理:LoongSuite 如何通过 OTel 扩展规范填补 AI Agent 可观测体系的语义空白?
agent
px不是xp2 小时前
【灶台导航】优化纠错实录
javascript·微信小程序
louisliao_19812 小时前
Hermes学习收集
agent
kyriewen2 小时前
开源|Image Harvest v1.0.5:AI 智能标签 + Eagle 导出,设计师和开发者的图片工作流神器
前端·javascript·ai编程
canonical_entropy3 小时前
自进化的两个尺度:RMSP Agent 与 AGE 方法论的深层结构对应
aigc·agent·ai编程
幺风3 小时前
A2UI 技术详解:让 AI Agent 学会“说界面”
agent