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 | 工具调用请求列表 |
finishReason | stop / tool_use / max_tokens / error |
无工具调用时的处理:
- 去除
<think>...</think>块(DeepSeek 推理格式) - 发送
text_delta进度事件 - 检查是否有待处理的后台子 Agent 结果
- 如有后台结果且未超过最大排空轮数(3 轮)→ 注入结果并继续循环
- 否则返回最终结果
第 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)"]
安全管道详细步骤:
- Abort 检查 —
signal.aborted为 true 则立即返回 - 原生搜索跳过 — 如果
nativeSearchEnabled且工具是原生搜索工具,跳过本地执行 - Channel 角色检查 —
channelUserPermissions.canUseTool === false时阻止所有工具 - PreToolUse Hook — 执行注册的钩子,
behavior === 'deny'时阻止 - ExecutionFirewall — 检查文件路径和命令是否触及禁止区域
- GuardianAgent — LLM 安全评估(如已启用)
- Permission Callback — 敏感工具需要用户确认
- 执行 — 带超时的工具执行(
Promise.race与超时 Promise 竞争) - PostToolUse Hook — 异步触发,不阻塞(fire-and-forget)
第 4 步:结果处理
- 输出截断 — 超过
TOOL_RESULT_MAX_CHARS(50KB)的结果被截断 - YieldSignal 检查 — 如果工具抛出
YieldSignal,立即结束循环 - 迭代完成回调 —
await onIterationComplete()保存消息到数据库 - 追加到消息列表 — 工具结果以
role: 'tool'消息追加 - 后台结果排空 — 将已完成的后台子 Agent 结果注入消息列表
第 5 步:循环控制
回到第 1 步,直到满足以下任一退出条件:
| 退出条件 | finishReason |
|---|---|
| LLM 返回纯文本(无工具调用) | stop |
| 达到最大迭代次数 | max_iterations |
| AbortSignal 触发 | interrupted |
| LLM 返回错误 | error |
| YieldSignal 触发 | stop |
常量定义
| 常量 | 值 | 说明 |
|---|---|---|
MAX_ITERATIONS | 40 | 最大循环迭代次数 |
TEMPERATURE | 0.1 | LLM 温度参数 |
MAX_TOKENS | 8192 | LLM 最大输出 token |
TOOL_RESULT_MAX_CHARS | 50,000 | 工具结果最大字符数 |
MAX_LLM_RETRIES | 2 | LLM 调用重试次数 |
LLM_RETRY_DELAY_MS | 1,000 | 重试基础延迟(ms) |
DEFAULT_TOOL_EXECUTION_TIMEOUT_MS | 300,000 | 单个工具执行超时(5 分钟) |
PERMISSION_TIMEOUT | 300,000 | 权限确认超时(5 分钟) |
MAX_BACKGROUND_DRAIN_ROUNDS | 3 | 最大连续后台排空轮数 |
DEFAULT_MAX_HISTORY | 100 | 最大历史消息加载数 |
VERSION | 0.1.0 | TinyElf 版本号 |
后台子 Agent 集成
TinyElf 支持后台子 Agent 并行执行。主循环通过以下机制与后台任务协作:
结果注入
pushBackgroundResult(result: BackgroundAgentResult): void
后台子 Agent 完成后,通过 SubagentManager.setBackgroundCompleteCallback 将结果推入主循环的队列。
排空策略
- 每次迭代结束时检查后台结果队列
- 有新结果时,以
[Background agent "label" (runId) outcome]格式注入 - 如果 LLM 返回纯文本但有待处理的后台结果,继续循环(最多 3 轮)
- 真正的工具调用会重置排空计数器
消息持久化
每次迭代完成时,通过 onIterationComplete 回调保存消息:
- 构建
MessageBlock[](thinking + tool_use 块) - 保存 assistant 消息(含工具调用信息)
- 逐个保存工具结果消息(含
tool_result块) - 通过 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 Loop | tinyelf/TinyElfAgentLoop.ts | 核心循环逻辑 |
| Engine | tinyelf/TinyElfEngine.ts | IEngine 实现 |
| Session Runner | tinyelf/TinyElfSessionRunner.ts | 会话执行编排 |
| LLM Adapter | tinyelf/TinyElfLLMAdapter.ts | LLM 调用适配 |
| Prompt Builder | tinyelf/TinyElfPromptBuilder.ts | 消息构建 |
| Types | tinyelf/types.ts | 类型和常量定义 |
所有路径相对于 packages/desktop/app/main/services/agent-core/engine/。
扩展点
- 自定义工具超时 —
TinyElfEngineConfig.toolExecutionTimeout - 自定义迭代上限 —
TinyElfEngineConfig.maxIterations或maxTurns - 自定义温度 —
TinyElfEngineConfig.temperature - Thinking 级别 —
TinyElfEngineConfig.thinkingLevel(none/low/medium/high) - Hook 扩展 — 通过
HookExecutor注册 PreToolUse / PostToolUse / SessionStart / SessionEnd 等钩子
相关模块
| 模块 | 路径 | 关系 |
|---|---|---|
| ToolRegistryBuilder | tinyelf/TinyElfToolRegistryBuilder.ts | 构建工具注册表 |
| SubagentManager | tinyelf/tools/SpawnTool.ts | 子 Agent 管理 |
| ExecutionFirewall | platform/security/ExecutionFirewall.ts | 第一层安全 |
| GuardianAgent | platform/security/GuardianAgent.ts | 第二层安全 |
| TinyElfPermissions | tinyelf/TinyElfPermissions.ts | 第三层安全 |