Skip to main content

TinyElf Agent 循环详解

TinyElf 是 Elftia 的内置 Agent 引擎,实现了经典的「LLM 调用 → 工具执行 → 结果观察 → 循环」模式。本文详细解析完整的 Agent Loop 算法。

整体架构

graph TB
Engine["TinyElfEngine"] --> SessionRunner["TinyElfSessionRunner<br/>runSession()"]

SessionRunner --> ToolBuilder["TinyElfToolRegistryBuilder<br/>buildToolRegistry()"]
SessionRunner --> PromptBuilder["TinyElfPromptBuilder<br/>buildMessages()"]
SessionRunner --> LoopInst["TinyElfAgentLoop<br/>实例化"]
SessionRunner --> |"运行"| Loop["AgentLoop.run()"]

ToolBuilder --> FileTools["FileSystemTools"]
ToolBuilder --> ShellT["ShellTool"]
ToolBuilder --> WebT["WebSearch/WebFetch"]
ToolBuilder --> SkillsT["Skills 工具"]
ToolBuilder --> SpawnT["SpawnTool"]
ToolBuilder --> McpT["MCP 工具"]
ToolBuilder --> SessionT["Session 工具"]

Loop --> LLMCall["callLLMWithRetry()"]
Loop --> ToolExec["executeToolCalls()"]
Loop --> Save["onIterationComplete()"]

会话执行流程

sequenceDiagram
participant R as AgentRouter
participant E as TinyElfEngine
participant B as ToolRegistryBuilder
participant S as SessionRunner
participant L as AgentLoop
participant LLM as TinyElfLLMAdapter
participant T as ToolRegistry

R->>E: startSession(ctx)
E->>E: saveMessage(user)
E->>E: sender.send(userMessage)
E->>B: buildToolRegistry()
B-->>E: { toolRegistry, subagentManager, ... }
E->>S: runSession(deps, params)
S->>S: new TinyElfLLMAdapter()
S->>S: new ExecutionFirewall()
S->>S: buildGuardian()
S->>L: new TinyElfAgentLoop()
S->>S: buildMessages()
S->>L: run(messages, callbacks)

loop 最多 40 次迭代
L->>LLM: callLLMWithRetry(messages, tools)
LLM-->>L: { content, toolCalls, usage }

alt 无工具调用
L->>L: 检查后台结果
alt 有后台结果
L->>L: 注入后台结果到消息
L->>L: continue (不消耗迭代)
else 无后台结果
L-->>S: AgentLoopResult (finishReason: stop)
end
else 有工具调用
L->>L: executeToolCalls(parallel)
Note over L,T: 安全管道: Firewall → Guardian → Permission
L->>T: execute(tool)
T-->>L: ToolCallResult
L->>S: onIterationComplete(data)
S->>S: saveMessage(assistant + tools)
S->>S: sender.send(assistantMessage)
L->>L: 追加工具结果到消息
L->>L: 排空后台结果
end
end

S->>S: saveFinalResult()
S->>S: sender.send(result + complete)

Agent Loop 算法

第 1 步:调用 LLM(带重试)

callLLMWithRetry(messages, tools, signal)
  • 最多重试 MAX_LLM_RETRIES(2)次
  • 重试延迟:LLM_RETRY_DELAY_MS * (attempt + 1)(递增 1s、2s、3s)
  • 重试条件:LLM 返回 finishReason === 'error' 且无内容,或抛出异常
  • 所有尝试失败后返回错误消息
  • 每次重试前检查 AbortSignal

第 2 步:解析响应

LLM 响应包含三部分:

字段说明
content文本回复(可能包含 <think> 块)
toolCalls工具调用请求列表
finishReasonstop / tool_use / max_tokens / error

无工具调用时的处理:

  1. 去除 <think>...</think> 块(DeepSeek 推理格式)
  2. 发送 text_delta 进度事件
  3. 检查是否有待处理的后台子 Agent 结果
  4. 如有后台结果且未超过最大排空轮数(3 轮)→ 注入结果并继续循环
  5. 否则返回最终结果

第 3 步:工具执行(并行)

同一 LLM 响应中的所有工具调用并行执行Promise.all)。每个工具调用经过以下安全管道:

graph LR
TC["工具调用请求"] --> Abort{"检查 Abort"}
Abort -->|已中止| Err1["返回错误"]
Abort -->|未中止| NS{"原生搜索跳过?"}
NS -->|是| Skip["跳过(provider 处理)"]
NS -->|否| Chan{"Channel 角色检查"}
Chan -->|禁止| Err2["角色无权限"]
Chan -->|允许| Hook{"PreToolUse Hook"}
Hook -->|deny| Err3["Hook 拒绝"]
Hook -->|allow| FW{"ExecutionFirewall"}
FW -->|blocked| Err4["防火墙拒绝"]
FW -->|allowed| GA{"GuardianAgent"}
GA -->|blocked| Err5["Guardian 拒绝"]
GA -->|allowed| Perm{"Permission Callback"}
Perm -->|deny| Err6["用户拒绝"]
Perm -->|allow| Exec["执行工具<br/>5 分钟超时"]
Exec --> PostHook["PostToolUse Hook<br/>(fire-and-forget)"]

安全管道详细步骤:

  1. Abort 检查signal.aborted 为 true 则立即返回
  2. 原生搜索跳过 — 如果 nativeSearchEnabled 且工具是原生搜索工具,跳过本地执行
  3. Channel 角色检查channelUserPermissions.canUseTool === false 时阻止所有工具
  4. PreToolUse Hook — 执行注册的钩子,behavior === 'deny' 时阻止
  5. ExecutionFirewall — 检查文件路径和命令是否触及禁止区域
  6. GuardianAgent — LLM 安全评估(如已启用)
  7. Permission Callback — 敏感工具需要用户确认
  8. 执行 — 带超时的工具执行(Promise.race 与超时 Promise 竞争)
  9. PostToolUse Hook — 异步触发,不阻塞(fire-and-forget)

第 4 步:结果处理

  1. 输出截断 — 超过 TOOL_RESULT_MAX_CHARS(50KB)的结果被截断
  2. YieldSignal 检查 — 如果工具抛出 YieldSignal,立即结束循环
  3. 迭代完成回调await onIterationComplete() 保存消息到数据库
  4. 追加到消息列表 — 工具结果以 role: 'tool' 消息追加
  5. 后台结果排空 — 将已完成的后台子 Agent 结果注入消息列表

第 5 步:循环控制

回到第 1 步,直到满足以下任一退出条件:

退出条件finishReason
LLM 返回纯文本(无工具调用)stop
达到最大迭代次数max_iterations
AbortSignal 触发interrupted
LLM 返回错误error
YieldSignal 触发stop

常量定义

常量说明
MAX_ITERATIONS40最大循环迭代次数
TEMPERATURE0.1LLM 温度参数
MAX_TOKENS8192LLM 最大输出 token
TOOL_RESULT_MAX_CHARS50,000工具结果最大字符数
MAX_LLM_RETRIES2LLM 调用重试次数
LLM_RETRY_DELAY_MS1,000重试基础延迟(ms)
DEFAULT_TOOL_EXECUTION_TIMEOUT_MS300,000单个工具执行超时(5 分钟)
PERMISSION_TIMEOUT300,000权限确认超时(5 分钟)
MAX_BACKGROUND_DRAIN_ROUNDS3最大连续后台排空轮数
DEFAULT_MAX_HISTORY100最大历史消息加载数
VERSION0.1.0TinyElf 版本号

后台子 Agent 集成

TinyElf 支持后台子 Agent 并行执行。主循环通过以下机制与后台任务协作:

结果注入

pushBackgroundResult(result: BackgroundAgentResult): void

后台子 Agent 完成后,通过 SubagentManager.setBackgroundCompleteCallback 将结果推入主循环的队列。

排空策略

  • 每次迭代结束时检查后台结果队列
  • 有新结果时,以 [Background agent "label" (runId) outcome] 格式注入
  • 如果 LLM 返回纯文本但有待处理的后台结果,继续循环(最多 3 轮)
  • 真正的工具调用会重置排空计数器

消息持久化

每次迭代完成时,通过 onIterationComplete 回调保存消息:

  1. 构建 MessageBlock[](thinking + tool_use 块)
  2. 保存 assistant 消息(含工具调用信息)
  3. 逐个保存工具结果消息(含 tool_result 块)
  4. 通过 IPC 发送 assistantMessage 事件

最终的纯文本回复通过 saveFinalResult() 单独保存,包含执行统计信息(inputTokens、outputTokens)。

SDK 双写(已移除,2026-05-19)

历史:TinyElf 原本通过 TinyElfSdkAdapter.convertSingleMessage() 把每条消息转成 SDK JSONL 风格并写入 sdk_records 表,目的是与 Claude SDK 共享 schema、跨引擎互通。但 sdk_records 整套机制(Claude SDK 侧的 IPC 镜像 + TinyElf 侧的合成)在 2026-05-19 整体下线 —— Claude SDK 侧 IPC schema 跟磁盘 JSONL schema 不兼容(详见 docs/dev/66_sdk_records/01_schema_divergence_investigation.md),SDK 0.3.x 已提供官方 SessionStore 接口(见 packages/desktop/app/main/services/agent-core/agent/SqliteSessionStore.ts)。TinyElf 现在只依赖 chat_messages 作为持久化源,无需 SDK 镜像。

sdkDualWrite state 字段、initSdkDualWrite / initSdkDualWriteForResume / writeSdkRecords 方法、TinyElfSdkAdapter.ts 文件均已删除。

关键文件

文件路径说明
Agent Looptinyelf/TinyElfAgentLoop.ts核心循环逻辑
Enginetinyelf/TinyElfEngine.tsIEngine 实现
Session Runnertinyelf/TinyElfSessionRunner.ts会话执行编排
LLM Adaptertinyelf/TinyElfLLMAdapter.tsLLM 调用适配
Prompt Buildertinyelf/TinyElfPromptBuilder.ts消息构建
Typestinyelf/types.ts类型和常量定义

所有路径相对于 packages/desktop/app/main/services/agent-core/engine/

扩展点

  • 自定义工具超时TinyElfEngineConfig.toolExecutionTimeout
  • 自定义迭代上限TinyElfEngineConfig.maxIterationsmaxTurns
  • 自定义温度TinyElfEngineConfig.temperature
  • Thinking 级别TinyElfEngineConfig.thinkingLevel(none/low/medium/high)
  • Hook 扩展 — 通过 HookExecutor 注册 PreToolUse / PostToolUse / SessionStart / SessionEnd 等钩子

相关模块

模块路径关系
ToolRegistryBuildertinyelf/TinyElfToolRegistryBuilder.ts构建工具注册表
SubagentManagertinyelf/tools/SpawnTool.ts子 Agent 管理
ExecutionFirewallplatform/security/ExecutionFirewall.ts第一层安全
GuardianAgentplatform/security/GuardianAgent.ts第二层安全
TinyElfPermissionstinyelf/TinyElfPermissions.ts第三层安全