Isshin AI Agent:LLM 工具路由架构

本文描述当前 Agent 的路由模式:如何用 LLM Tool Calling 替代关键词意图识别,在本地工作区沙箱内完成多步 ReAct,再把真实数据交给对话模型生成最终回复。


1. 设计动机

早期实现用 thoughtNode + 关键词枚举判断用户意图(列目录、读文件、搜代码等)。这种方式有三个根本问题:

问题 表现
意图覆盖不全 新说法、口语化追问(「列给我」「那个项目呢」)容易漏判
上下文断裂 多轮对话中的指代(「它」「上面那个」)难以用规则处理
维护成本高 每加一种场景就要改关键词表,逻辑膨胀且互相干扰

当前方案采用业界主流的 LLM Tool Calling + ReAct 循环 :由模型根据 system prompt 和工具 schema 自行决定 是否调用工具、调用哪个、参数是什么,并可多步串联(先 listread)。

路由与回复职责分离------路由器只负责「拿数据」,对话模型只负责「讲清楚」。


2. 总体架构

css 复制代码
用户消息
   │
   ▼
┌─────────────────────────────────────┐
│  Phase A: Agent 路由循环             │
│  runAgentLoop()                     │
│  LLM (非流式, tools API)            │
│    ↔ 工具执行 (Tauri → Rust 沙箱)    │
│  最多 5 步, 产出 observation        │
└─────────────────────────────────────┘
   │
   ▼ observation(Markdown 格式的工具执行记录)
┌─────────────────────────────────────┐
│  Phase B: 对话补全                   │
│  streamChatCompletion()             │
│  LLM (流式, 无 tools)               │
│  system 注入 observation + 人设      │
└─────────────────────────────────────┘
   │
   ▼
用户可见的流式回复

关键设计:两次 LLM 调用,两种角色

阶段 API 流式 携带 tools 职责
Agent 路由 chatCompletionWithTools 规划并执行本地工具
用户回复 streamChatCompletion 解读 observation,自然语言作答

每条用户消息始终先走 Phase A :由路由器判断是否需要访问本地工作区;若有工具执行,UI 展示 Agent 状态条(规划 / 执行 / 整理),Phase B 再携带 ISSHIN_AGENT_PERSONA 与 observation 生成流式回复。


3. 核心模块

bash 复制代码
src/agent/
├── tools.ts      # 工具 schema、路由器 system prompt、步数/超时常量
├── graph.ts      # ReAct 主循环 runAgentLoop()
├── executor.ts   # 工具执行、参数清洗、结果格式化
├── schema.ts     # AgentGraphState / AgentStep 类型
└── prompt.ts     # 面向用户的 Isshin 人设(Phase B 使用)

src/services/chat.ts   # chatCompletionWithTools / streamChatCompletion
src/hooks/useAppState.ts  # 串联 Agent → 流式回复
src-tauri/src/workspace.rs  # 路径沙箱、排除目录、读/搜限制

3.1 路由器 (tools.ts)

定义 4 个 OpenAI 兼容的 function tools:

工具 用途 典型场景
list_work_dir 列目录 「work 里有哪些项目」
read_work_file 读单文件 「看看 package.json」
search_work_text 全文搜索 「哪里用到了 useState」
analyze_project 项目内搜+读源码(≤5 文件) 「单词卡片怎么生成的」

ROUTER_SYSTEM_PROMPT 明确路由器不是最终回复者,规则包括:路径用相对路径、信息足够时停止调工具、纯闲聊不调工具、禁止编造结果。

3.2 ReAct 循环 (graph.ts)

runAgentLoop() 是 Phase A 的入口,逻辑可概括为:

scss 复制代码
for step in 0..AGENT_MAX_STEPS (5):
  1. 组装 messages = [router system, 用户请求+近期对话, ...历史 tool 消息]
  2. 调用 chatCompletionWithTools(tool_choice: "auto")
  3. 若无 tool_calls → 结束循环(认为信息已够或无需工具)
  4. 对每个 tool_call:
       - 白名单校验
       - executeWorkspaceTool()
       - 将 tool 结果以 role: "tool" 写回 reactMessages
  5. 进入下一步,LLM 根据 tool 结果决定继续调工具或停止

循环结束后,将所有步骤格式化为 observation(Markdown),供 Phase B 注入。

reactMessages 遵循 OpenAI 多轮 tool calling 协议:

scss 复制代码
assistant (含 tool_calls) → tool (含 tool_call_id + JSON 结果) → assistant → ...

3.3 工具执行层 (executor.ts)

前端 executor 是 Rust 沙箱的薄封装,额外承担:

  • 参数清洗 :剥离 ... 路径段;搜索词限 200 字符
  • 项目名模糊匹配 :Levenshtein 距离 ≤4 时自动纠正拼写(isshin-AI-TextField → 真实目录名)
  • 目录误读兜底read_work_file 目标是项目文件夹时,自动读 README / package.json 或列目录
  • analyze_project 编排:项目内搜索 → 取最多 5 个相关文件 → 单文件内容超 8KB 截断

执行结果以 JSON 返回给路由器;同时 formatToolResultMarkdown() 生成人类可读的 observation 片段。

3.4 沙箱 (workspace.rs)

Rust 层是最终安全边界:

  • 根目录固定:/Users/miles_wang/Desktop/work
  • canonicalize + starts_with 防止路径穿越
  • 自动跳过 node_modules.gittargetdist
  • 单文件读取上限 512KB;搜索扫描单文件上限 256KB

Agent 只有读权限,无写、无执行 shell。


4. 端到端数据流

以用户提问「Isshin-Etymonix-AI 里单词卡片怎么生成的」为例:

markdown 复制代码
1. useAppState.sendMessage()
      recentMessages ← 最近 6 条 user/assistant 消息(供指代消解)

2. runAgentLoop()
      Step 1: LLM → analyze_project(projectName, query)
      Step 2: (若需要) LLM → read_work_file 补充细节
      Step 3: LLM → tool_calls 为空,结束

3. observation 示例结构:
      ## 步骤 1:`analyze_project`
      ### analyze_project --- `Isshin-Etymonix-AI`
      #### `src/components/Card.tsx`
      ```tsx
      ...

4. streamChatCompletion()
      system: ISSHIN_AGENT_PERSONA + observation
      user:   原始问题 + observation(双通道注入,防模型忽略 system)
      → 流式输出解读后的回答

Phase B 的 system prompt 明确要求:工具已由应用执行完毕,禁止再输出 [TOOL_CALL] 或让用户手动跑命令sanitizeAssistantContent 在展示层做最后一道过滤。


5. 护栏机制

护栏 实现 触发后果
工具白名单 ALLOWED_TOOL_NAMES 返回 { ok: false, error },不执行
路径清洗 sanitizeRelativePath 剥离 ..,防目录穿越
步数上限 AGENT_MAX_STEPS = 5 记录错误,用已有 observation 继续
单步超时 AGENT_STEP_TIMEOUT_MS = 45s abort 当前 LLM 请求
总循环超时 AGENT_LOOP_TIMEOUT_MS = 120s 终止整个 Agent 循环
路由失败降级 graph.ts catch 第一步失败 → routedBy: "none",跳过工具,直接进入 Phase B
假工具调用过滤 stripFakeToolCalls UI 层剔除模型幻觉的 [TOOL_CALL] 文本

6. Agent 交互与 Prompt 策略

UI 反馈

useAppState 在 Phase A 期间插入 agent-status 消息,随 onPhase 回调更新:

phase 用户可见文案
thought 意图识别 / 第 N 步规划工具
action 正在执行本地工具:list_work_dir
observation 整理观察结果
done 路由器摘要(如「已完成 2 步工具调用:list → read」)

若未触发任何工具(shouldAct === false),状态条会被移除,直接进入 Phase B 流式回复。

Phase B Prompt 组成

makefile 复制代码
system:
  ISSHIN_AGENT_PERSONA          # 人设、工作区范围、禁止假工具调用
  + observation(若有)         # 要求基于代码梳理调用链与实现逻辑

user:
  原始问题
  + observation(双通道注入)    # 防止模型忽略 system 中的工具结果

分析类问题(analyze_project)的 observation 可能含多个源码文件;system 明确要求解读实现逻辑,而非复述 README。


7. 状态模型

typescript 复制代码
interface AgentGraphState {
  thought: string | null;       // 路由器结束时的摘要
  shouldAct: boolean;           // 是否实际执行过工具
  steps: AgentStep[];           // 每步工具名、参数、结果
  observation: string | null;   // 注入 Phase B 的 Markdown
  routedBy: "llm" | "none";    // 是否走路由
  errorMessage: string | null;  // 超时、步数耗尽等
}

AgentStep 同时保存 resultMarkdown(展示用)和原始 JSON(通过 reactMessages 传给下一轮路由器)。


8. 与旧架构的对比

scss 复制代码
旧:用户消息 → 关键词 thoughtNode → 硬编码 action → observation → 流式回复
新:用户消息 → LLM 选工具 → executor → (循环) → observation → 流式回复

删除的是 nodes.ts 中约 800 行的意图枚举;新增的是 tools.ts + executor.ts + graph.ts 的分层,以及 chatCompletionWithTools API 封装。

权衡:

  • ✅ 意图理解质量显著提升,多轮追问可用
  • ✅ 新工具只需加 schema + executor case,无需改路由规则
  • ⚠️ 依赖模型对 OpenAI tools API 的支持质量(部分模型会幻觉 tool call 文本,靠 prompt + 后处理缓解)
  • ⚠️ 每次用户消息至少 1 次非流式 LLM 调用(路由),有工具时可能 2--5 次,整体延迟高于无工具的闲聊

9. 扩展指南

新增一个工作区能力(例如「读 git log」)的标准步骤:

  1. workspace.rs --- 实现 Tauri command + 沙箱校验
  2. tools.ts --- 添加 WORKSPACE_TOOLS 条目 + ALLOWED_TOOL_NAMES
  3. executor.ts --- executeWorkspaceTool 增加 case + formatToolResultMarkdown 格式化
  4. ROUTER_SYSTEM_PROMPT --- 补充 1--2 句使用场景说明

无需修改 graph.ts 循环逻辑------这是分层带来的主要收益。


10. 调试

  • LLM 控制台 (独立窗口):可查看每次 Agent 路由 (tools) 请求的完整 request/response,包括 tool_calls 原始 JSON
  • Agent 状态条 :实时显示当前 phase(规划 / 执行 list_work_dir / 整理观察)
  • observation 内容:最终注入 system 的 Markdown 即 Agent 实际读到的数据,是排查「模型胡说」的第一现场

附录:关键常量

typescript 复制代码
WORKSPACE_ROOT          = "/Users/miles_wang/Desktop/work"
AGENT_MAX_STEPS         = 5
AGENT_LOOP_TIMEOUT_MS   = 120_000   // 2 分钟
AGENT_STEP_TIMEOUT_MS   = 45_000    // 45 秒
MAX_ANALYZE_FILES       = 5         // analyze_project 最多读取文件数
MAX_FILE_CONTENT_IN_OBS = 8_000     // 单文件注入 observation 的字符上限
相关推荐
孟健2 小时前
GLM-5.2能打了,但还不能替代GPT
ai编程
赫媒派5 小时前
Anthropic用Claude处理95%查询的实战
ai编程
kartjim5 小时前
我用 AI 一小时写了一个世界杯数据可视化平台|前端 VibeCoding 初体验
前端·程序员·ai编程
唐老板5 小时前
三个工具单拎都很猛,拼在一起才是完全体
ai编程
搬砖的码农5 小时前
(05)进程一关对话就没了:聊天记录怎么存、重启怎么恢复
前端·agent·ai编程
乘风gg7 小时前
还在养虾吗?虾王已诞生:微信龙虾 ClawBot
前端·ai编程·claude
ServBay7 小时前
Laravel Herd MCP 的替代,多语言与跨平台的 AI 本地开发选择
后端·ai编程·mcp
沉默王二9 小时前
讯飞版Codex+GLM-5.2=顶级世界杯AI搭子
agent·ai编程