AI-CLI 深度笔记
从源码出发,逐行解析终端 AI 工程师的每个模块 —— 不是概念介绍,而是真正的工程拆解
ReAct 循环引擎
Agent 的心脏:思考 → 行动 → 观察的无限循环,直到任务完成或需要人类介入。本章深入 AgentSession.run() 的每一行代码。
上下文初始化:7 层消息注入
initializeContext() 是每次会话启动时最关键的函数。它不是简单地拼接用户输入,而是按照 7 层优先级依次注入上下文信息,确保 LLM 在第一轮推理时就拥有足够的背景知识。这 7 层分别是:系统提示词(System Prompt)、项目规则文件(.ai-cli.md)、工作区上下文(Git 状态等)、长期记忆(语义搜索匹配)、会话记忆(本次会话目标与已完成动作)、用户偏好、以及最终的用户输入。
值得注意的是,长期记忆的注入使用了语义搜索而非关键词匹配。系统会将用户输入向量化,在 SQLite 中搜索最相关的 3 条记忆。这意味着即使用户使用了不同的措辞,只要语义相关,历史决策和偏好仍然会被召回。例如,用户上次说过"我偏好函数式组件",这次问"帮我写个列表页面",系统依然会注入该偏好。
export async function initializeContext(
config: AgentSessionConfig,
events: AgentStreamCallbacks,
userInput: string,
): Promise<InitializedContext> {
const aiMessages: Message[] = [];
// 1. System Prompt — 注入工具定义和项目指令
const toolDefs = mcpManager.getToolDefinitions();
const systemPrompt = buildSystemPrompt({
tools: toolDefs,
projectInstructions,
cwd
});
aiMessages.push({ role: 'system', content: systemPrompt });
// 2. Project rules file (.ai-cli.md) — 项目级约定
const { content: rulesContent, hasRules } = loadProjectRules(cwd);
if (hasRules)
aiMessages.push({ role: 'system', content: formatRulesForContext(rulesContent) });
// 3. Workspace context — git分支、最近提交、已修改文件
const ctx = getCachedContext() || await captureContext(cwd);
if (ctx.workspaceName)
aiMessages.push({ role: 'system', content: formatContextForPrompt(ctx) });
// 4. Long-term memories — 语义搜索匹配(最多3条)
const memories = await smartSearchMemory(userInput, chatFn, { limit: 3, cwd });
if (memories.length > 0)
aiMessages.push({ role: 'system', content: `[相关记忆]\n${memories.map(m => m.content).join('\n')}` });
// 5. Session memory — 当前会话目标与已完成动作
const loaded = loadSessionMemory(sessionId, cwd);
if (loaded && (loaded.goal || loaded.completedActions.length > 0))
aiMessages.push({ role: 'system', content: formatSessionMemoryForContext(loaded) });
// 6. User preferences — 用户偏好设置
const prefs = getUserPreferences(cwd);
if (prefs.length > 0)
aiMessages.push({ role: 'system', content: `[用户偏好]\n${prefs.join('\n')}` });
// 7. User input — 最终的用户输入
aiMessages.push({ role: 'user', content: userInput });
return { messages: aiMessages, toolDefs };
}流式回调类型体系
AgentSession 与外界的所有通信都通过 AgentStreamCallbacks 完成。这个接口定义了 7 个回调函数,覆盖了从文本输出到工具执行的完整生命周期。每个回调都在特定时机被触发,使得上层(CLI 界面、Web GUI、守护进程)可以实时感知 Agent 的状态变化。
其中 onStatus 回调特别重要——它报告 Agent 当前处于 thinking、executing、confirming、compressing 还是 idle 状态。Web GUI 用这个状态来渲染不同的 UI 动画,守护进程用它来记录任务进度日志。
export interface AgentStreamCallbacks {
onText: (delta: string, fullContent: string) => void;
onToolCalls: (calls: ToolCallRequest[]) => void;
onToolResults: (results: ToolResultEntry[]) => void;
onStatus: (status: AgentStatus) => void;
onError: (error: string) => void;
onComplete: (result: AgentCompleteResult) => void;
onTokenUsage?: (usage: { input: number; output: number }) => void;
}
export interface AgentStatus {
type: 'thinking' | 'executing' | 'confirming' | 'compressing' | 'idle';
detail?: string;
}
export interface LLMStreamResult {
content: string;
tool_calls?: ToolCallRequest[];
finishReason?: 'stop' | 'length' | 'tool_calls' | 'content_filter' | 'aborted' | 'error';
usage?: { input: number; output: number };
reasoning_content?: string;
}偏航检测:防止 Agent 无限空转
DriftSignals 是 ReAct 循环中最容易被忽视却极其重要的子系统。它的作用是监控 Agent 的每一轮迭代,判断是否陷入了无效循环。如果不做偏航检测,Agent 可能会反复调用同一个工具、持续返回空结果、或者产出越来越短的内容——白白消耗 token 而无法完成任务。
系统定义了 6 个核心信号:consecutiveFailures(连续失败次数)、noProgressTurns(无进展轮数)、outputLengthDrop(输出长度骤降比)、repeatedToolFingerprints(重复工具指纹)、refusalCount(拒绝执行次数)以及 pendingToolCount(待执行工具数)。
当连续失败达到 8 次或空闲轮数超过 10 轮时,系统判定为严重偏航,直接终止循环并向用户报告。轻度偏航时,系统会尝试自动纠偏策略,如注入提示让 LLM 换个思路、或者主动向用户询问更多信息。
const CONSECUTIVE_FAILURES_THRESHOLD = 4; // 连续4次失败 → 警告
const NO_PROGRESS_TURNS_THRESHOLD = 5; // 连续5轮无进展 → 警告
const OUTPUT_LENGTH_DROP_RATIO = 0.6; // 输出长度降至60% → 可疑
const REPEATED_TOOL_PATTERN_THRESHOLD = 5; // 同一工具模式5次 → 可疑
const SEVERE_FAILURES_THRESHOLD = 8; // 连续8次失败 → 终止循环
const SEVERE_IDLE_THRESHOLD = 10; // 连续10轮空闲 → 终止循环
export interface DriftSignals {
consecutiveFailures: number; // 连续工具执行失败计数
noProgressTurns: number; // 无有效输出轮数
recentAvgOutputLength: number; // 近期平均输出长度(基准)
currentOutputLength: number; // 当前输出长度
refusalCount: number; // LLM 拒绝执行计数
recentToolFingerprints: string[]; // 最近N轮的工具调用指纹
pendingToolCount: number; // 待确认/执行的工具数
hasToolCalls: boolean; // 本轮是否调用了工具
hasTextOutput: boolean; // 本轮是否有文本输出
turnNumber: number; // 当前迭代轮次
}错误恢复机制
除了偏航检测,AgentSession 还内置了多种错误恢复策略。injectLengthContinuation 在 LLM 因 finishReason === "length" 截断时自动注入续写提示,让 LLM 继续未完成的输出。checkEmptyReply 检测到空响应时会重新发送请求。isRepeatingSameTool 检测到 LLM 反复调用同一工具时,会注入反思提示,引导 LLM 改变策略。
compressIfNeeded() 是另一个关键的恢复机制。当上下文消息超过模型 token 限制时,系统会自动压缩历史消息——保留最近几轮的完整对话,将更早的轮次压缩为摘要。这确保了长会话不会因为上下文溢出而崩溃。
💡 核心洞察:ReAct 循环的精髓不在于「循环」本身,而在于每一轮迭代后的质量保障。偏航检测防止空转、错误恢复保证鲁棒性、上下文压缩避免溢出——这三个子系统共同确保了 Agent 在面对复杂任务时能够持续产出有效结果,而不是在某个环节卡死后无限循环。这正是 AI-CLI 与简单的 LLM API 调用的本质区别。
四层记忆系统
从会话到长期,AI 如何记住你的项目、偏好和历史决策。深入 autoExtract、AutoDream、语义搜索的源码实现。
| 层 | 名称 | 存什么 | 持久性 | 触发时机 |
|---|---|---|---|---|
| L1 | 会话记忆 | 完整对话历史 | SQLite 全量存储 | 每轮迭代 |
| L2 | 工作上下文 | Git状态/项目规则/任务看板 | SQLite + 文件缓存 | 会话初始化 |
| L3 | 长期记忆 | 结构化事实/偏好/决策 | SQLite 索引 + 语义搜索 | initializeContext 语义匹配 |
| L4 | 自动提取 | LLM 事后分析 → 提取关键信息 | 写入 L3 | 会话结束 / AutoDream |
L4 自动提取:autoExtractMemories() 源码解析
autoExtractMemories() 在每次会话结束时被调用。它不是简单地存储所有对话,而是用 LLM 对对话内容进行事后分析,只提取真正有价值的信息。提取 prompt 明确要求忽略闲聊、问候和中间过程,只保留偏好、决策、事实和命令四类信息。
每条提取结果都有 confidence 字段(0-100),只有置信度 ≥ 70 的条目才会被写入长期记忆。同时,系统还内置了去重机制,避免重复提取相同的信息。每日提取次数限制为 10 次(DAILY_LIMIT),防止频繁调用浪费 token。
const DAILY_LIMIT = 10; // 每日最多提取10次
const EXTRACTION_PROMPT = `分析以下对话,只提取**真正有价值**的信息。输出纯 JSON 数组:
[
{ "type": "preference", "content": "用户偏好...", "confidence": 80 },
{ "type": "decision", "content": "决定使用...", "confidence": 90 },
{ "type": "fact", "content": "项目事实...", "confidence": 60 }
]
类型: preference(偏好), decision(决策), fact(事实), command(命令)
置信度: 0-100。80+:用户明确陈述;60-79:可推断;<60:不确定。
忽略闲聊、问候、中间过程和失败尝试。`;
export async function autoExtractMemories(
messages: Message[],
cwd: string,
chatFn: (msgs: Message[]) => Promise<string>
): Promise<number> {
resetDailyIfNeeded(); // 每日重置计数器
if (todayCount >= DAILY_LIMIT) return 0; // 超过每日限额
const valuable = messages.filter(m => m.role === 'user' || m.role === 'assistant');
if (valuable.length < 3) return 0; // 消息太少,不值得提取
// LLM 提取 + 置信度过滤(>=70) + 去重
const extractionMsgs = [
{ role: 'system', content: EXTRACTION_PROMPT },
{ role: 'user', content: JSON.stringify(valuable.slice(-20)) } // 只取最近20条
];
const raw = await chatFn(extractionMsgs);
const items = JSON.parse(raw)
.filter((item: any) => item.confidence >= 70); // 置信度阈值
let saved = 0;
for (const item of items) {
const isDup = await checkDuplicate(item.content, cwd); // 去重检查
if (!isDup) {
await saveMemory({ type: item.type, content: item.content, cwd });
saved++;
}
}
todayCount += saved;
return saved;
}AutoDream:后台自动整理记忆
AutoDream 是比 autoExtract 更深层的记忆整理机制。它不是在每次会话结束时触发,而是在后台条件满足时才运行。这个设计灵感来源于人类睡眠时的记忆整理——不是每时每刻都在整理,而是在积累了足够经验后,找个时间后台整理。
四道门控各有明确的设计意图:时间门(24小时)避免频繁整理;会话门(5次会话)确保积累了足够素材;锁门(30分钟过期)防止并发执行;预算门确保不会因整理记忆而耗尽 API 额度。
const TIME_GATE_MS = 24 * 60 * 60 * 1000; // 24小时
const SESSION_GATE_COUNT = 5; // 5次会话
const LOCK_STALE_MS = 30 * 60 * 1000; // 30分钟锁过期
export function checkAutoDream(cwd: string): boolean {
// 1. 时间门控:距上次dream已超过24小时
const lastDream = getLastDreamTime(cwd);
if (Date.now() - lastDream < TIME_GATE_MS) return false;
// 2. 会话门控:自上次dream后已有5次会话
const sessionsSince = getSessionsSinceLastDream(cwd);
if (sessionsSince < SESSION_GATE_COUNT) return false;
// 3. 锁门控:没有正在运行的dream
const lock = getDreamLock(cwd);
if (lock && Date.now() - lock.timestamp < LOCK_STALE_MS) return false;
// 4. 预算门控:token预算充足
const budget = getTokenBudget(cwd);
if (budget.remaining < DREAM_MIN_BUDGET) return false;
return true; // 全部通过,可以触发AutoDream
}记忆搜索:从关键词到语义匹配
searchMemory() 实现了多层过滤的记忆检索。首先对查询进行分词,然后通过 SQL LIKE 进行预过滤来减少候选行数。接着匹配标签(#tag 格式),再根据重要性分数进行加权排序。这种混合策略兼顾了检索速度和准确性——纯向量搜索在大规模数据上较慢,而纯关键词搜索会遗漏语义相关但措辞不同的记忆。
工作区上下文捕获
export async function captureContext(cwd: string): Promise<WorkspaceContext> {
// 并行执行4个git命令,减少等待时间
const [gitBranch, gitRemote, gitLog, gitStatus] = await Promise.all([
execSafe(['git', 'branch', '--show-current'], cwd), // 当前分支
execSafe(['git', 'remote', 'get-url', 'origin'], cwd), // 远程仓库
execSafe(['git', 'log', '--oneline', '-5'], cwd), // 最近5条提交
execSafe(['git', 'status', '--short'], cwd), // 文件变更状态
]);
return {
workspaceName: path.basename(cwd),
gitBranch: gitBranch.stdout,
gitRemote: gitRemote.stdout,
recentCommits: gitLog.stdout.split('\n').filter(Boolean),
modifiedFiles: gitStatus.stdout.split('\n').filter(Boolean),
};
}记忆管理命令
/remember <内容>手动保存一条记忆到L3长期记忆库
/recall <关键词>语义搜索长期记忆,返回最相关结果
/forget <id>删除指定ID的记忆条目
/compact手动压缩当前上下文,释放token空间
/dream手动触发AutoDream整理记忆
/memory stats查看记忆库统计信息(数量/类型分布)
💡 核心洞察:四层记忆的设计哲学是 「越用越快」。同类问题第二次遇到时,L3 长期记忆已存有相关事实,LLM 不需要从零分析项目结构,速度提升数倍。而 L4 自动提取确保了记忆的积累不需要用户手动干预——这是系统级的设计,而非可选功能。AutoDream 的四道门控则体现了 「整理是有成本的」 这一工程现实:只有积累到足够有价值时,才值得花费 token 去整理。
MCP 工具系统
23 个内置工具分 7 大类,覆盖从文件操作到 Agent 调度的全链路。深入 executeSingleTool() 的完整生命周期,以及外部 MCP 的 stdio/SSE 接入方式。
executeSingleTool() 完整生命周期
每次工具调用都经过 executeSingleTool() 这个统一的执行入口。这个函数负责从 JSON 参数解析、安全分类、工具执行、结果持久化到重试跟踪的完整流程。它接受一个 retryTracker Map 来跨轮次跟踪工具调用的重试次数,防止 LLM 反复用相同参数调用同一失败工具。
参数解析使用 JSON.parse(tc.arguments),失败时直接返回错误信息而非抛异常。安全分类分为两级:classifyTool(toolDef) 基于工具声明确定静态类别,runtimeClassify() 结合运行时危险检查确定最终类别。工具指纹(toolFingerprint())用于识别「同一工具+相似参数」的重复调用模式。
export async function executeSingleTool(
tc: ToolCallRequest,
toolCtx: ToolExecutionContext,
retryTracker: Map<string, number>,
cachedDangerResult?: 'safe' | { level: 'caution' | 'danger'; reason: string },
): Promise<ToolResultEntry> {
// Step 1: 解析参数
let parsedArgs: Record<string, unknown>;
try { parsedArgs = JSON.parse(tc.arguments); }
catch { return { error: 'JSON解析失败' }; }
// Step 2: 查找工具定义 + 静态分类
const toolDef = mcpManager.getTool(tc.name);
const staticCat = classifyTool(toolDef);
// Step 3: 运行时危险检查(dangerCheck 函数)
const dangerCheckResult = cachedDangerResult
?? toolDef?.dangerCheck?.(parsedArgs, toolCtx.cwd);
const errorCategory = runtimeClassify(staticCat, tc.name, dangerCheckResult);
// Step 4: 执行工具
let toolResult = await mcpManager.executeTool(
{ name: tc.name, arguments: parsedArgs }, toolCtx
);
// Step 5: 应用上下文修改器
if (toolResult.contextModifier) toolResult.contextModifier(toolCtx);
// Step 6: 重试指纹跟踪
let retryCount = 0;
if (!toolResult.success) {
const fp = toolFingerprint(tc.name, parsedArgs);
retryCount = (retryTracker.get(fp) ?? -1) + 1;
retryTracker.set(fp, retryCount);
}
return { ...toolResult, retryCount, errorCategory };
}大结果持久化:避免上下文爆炸
工具执行的结果可能非常大(例如 shell 命令输出数百行日志,或 file-system 读取大文件)。如果将这些完整结果注入消息列表,会迅速耗尽上下文窗口。
persistLargeResult() 在结果超过 3000 字符时自动触发:将完整内容保存到 /tmp/ai-cli-results/ 目录,消息列表中只保留摘要(前30行 + 后10行 + 省略提示)。同时,后台异步清理超过 24 小时的临时文件。
const RESULT_THRESHOLD = 3000; // 超过3000字符触发持久化
export function persistLargeResult(toolName: string, content: string) {
if (content.length <= RESULT_THRESHOLD) return { summary: content };
// 保存完整结果到临时文件
const timestamp = Date.now();
const filePath = `/tmp/ai-cli-results/${timestamp}-${toolName}.txt`;
fs.writeFileSync(filePath, content, 'utf-8');
// 生成摘要:前30行 + 最后10行 + 省略提示
const lines = content.split('\n');
const head = lines.slice(0, 30).join('\n');
const tail = lines.slice(-10).join('\n');
const omitted = lines.length - 40;
const summary = `${head}\n... (省略 ${omitted} 行) ...\n${tail}`;
// 异步清理24小时前的文件
scheduleCleanup(24 * 60 * 60 * 1000);
return {
summary,
filePath,
totalLines: lines.length,
totalSize: content.length,
};
}外部 MCP 支持:stdio 与 SSE 双协议
除了 23 个内置工具,AI-CLI 支持通过 stdio(本地子进程通信)和 SSE(Server-Sent Events 远程服务)两种协议接入外部 MCP 服务器。stdio 模式下,AI-CLI 会启动一个子进程(如 npx @modelcontextprotocol/server-filesystem),通过标准输入输出进行 JSON-RPC 通信。SSE 模式下,AI-CLI 连接到远程 HTTP 端点(如 GitHub MCP API),通过长连接接收事件。
每个外部工具可单独配置 dangerLevelOverride 覆盖默认安全级别。例如,将 GitHub 的 search_repositories 设为 safe 自动放行,但 create_issue 设为 danger 强制确认。这种细粒度控制确保了外部工具的安全性不会成为系统的薄弱环节。
💡 实用技巧:工具系统的设计遵循了 「管道 + 过滤器」 模式。每个工具是独立的过滤器,输入参数经过 Zod 校验、安全分类、执行、结果持久化、重试跟踪等多层管道处理。这种架构使得新增工具只需定义 schema + execute 函数,无需关心安全、持久化、重试等横切关注点。而 contextModifier 机制更允许工具在执行后修改全局上下文,例如 git commit 后自动更新工作区上下文。
安全引擎
三级安全 + AI 分类器:安全操作自动放行,危险操作必须确认。深入分类器 prompt 设计和运行时危险检查的实现细节。
| Level | 名称 | 行为 | 典型工具 |
|---|---|---|---|
| safe | 安全 | 自动执行,无需确认 | 读文件、搜索代码、ls/cat |
| caution | 注意 | AI 分类器动态判断 | 写文件、shell命令、npm install |
| danger | 危险 | 必须用户确认 | 删除文件、git push、rm -rf |
AI 分类器:完整 Prompt 与决策逻辑
AI 分类器是安全引擎最核心的创新。传统的做法是将工具声明为固定的 safe/dangerous,但这种方式无法处理「同一工具在不同参数下安全性不同」的情况。例如 shell 工具执行 ls 是安全的,但执行 rm -rf / 是危险的。AI 分类器用轻量 LLM 在 200-500ms 内对 caution 级别的工具调用做出动态判断。
分类器的 system prompt 精心设计了一张常见场景判断表,覆盖了文件操作、Shell 命令、Git 操作等主要场景。输出格式要求严格 JSON,只返回 {"decision":"safe"|"risky","reason":"..."}。这种约束确保了解析的可靠性和低延迟。
const CLASSIFIER_SYSTEM_PROMPT = [
'你是工具安全分类器。判断一次工具调用是否可以在不询问用户的情况下自动执行。',
'决策标准:',
'- "safe" = 可以自动执行,无需用户确认',
'- "risky" = 需要用户确认后再执行',
'',
'常见场景判断:',
'| 读取文件/搜索代码 | safe | 无害操作 |',
'| 写入/修改现有文件 | risky | 可能覆盖重要内容 |',
'| 创建新文件 | safe | 新增内容不破坏已有 |',
'| 删除文件/目录 | risky | 不可逆操作 |',
'| shell: ls/cat/grep/pwd/which | safe | 只读命令 |',
'| shell: rm/mv/cp/dd/format | risky | 破坏性命令 |',
'| shell: mkdir/touch | safe | 创建行为无害 |',
'| shell: npm/pip/cargo install | risky | 修改依赖锁文件 |',
'| shell: git commit/push | risky | 不可逆版本操作 |',
'| 编译/构建 | safe | 构建输出可预览 |',
'',
'输出格式(仅 JSON):',
'{"decision":"safe"|"risky","reason":"简短解释(10-30字)"}',
].join('\n');
export async function classifyToolAction(
options: ClassifyOptions
): Promise<ClassifyResult> {
// 使用轻量 LLM 调用,延迟 200-500ms
// 输入:工具名 + 参数 + 工作上下文
// 输出:{ decision: 'safe' | 'risky', reason: string }
const result = await lightweightLLMCall({
system: CLASSIFIER_SYSTEM_PROMPT,
user: JSON.stringify({
tool: options.toolName,
args: options.args,
context: options.workingContext,
}),
maxTokens: 100, // 极短输出,保证速度
});
return JSON.parse(result);
}dangerCheck 运行时检查
// 每个工具可以定义自己的 dangerCheck 函数
// shell 工具的 dangerCheck 示例:
function shellDangerCheck(args: { command: string }, cwd: string) {
const cmd = args.command.trim().toLowerCase();
const dangerousPatterns = [
/rm\s+-rf/, /format/, /dd\s+if=/, // 破坏性命令
/git\s+push/, /git\s+reset\s+--hard/, // 不可逆Git操作
/npm\s+publish/, /docker\s+rm/, // 发布/删除操作
/curl.*\|\s*sh/, /wget.*\|\s*bash/, // 远程脚本执行
];
const safePatterns = [
/^ls/, /^cat/, /^grep/, /^find/, /^pwd/, // 只读命令
/^echo/, /^mkdir/, /^touch/, /^which/, // 创建/查询
/^git\s+status/, /^git\s+log/, /^git\s+diff/, // Git只读
];
for (const p of dangerousPatterns) {
if (p.test(cmd)) return { level: 'danger', reason: '检测到破坏性命令模式' };
}
for (const p of safePatterns) {
if (p.test(cmd)) return 'safe';
}
return { level: 'caution', reason: '需要AI分类器判断' };
}💡 核心洞察:AI 分类器的价值在于 减少 50-80% 确认弹框。传统的安全模型只有 safe/danger 两级,导致大量实际安全的操作(如写入新文件、执行 ls 命令)也需要确认。caution 级别的引入 + AI 动态判断,让系统能够根据具体参数做更精确的决策。分类器使用轻量 LLM,单次延迟仅 200-500ms,相比频繁打断用户的工作流,这点延迟完全可以接受。
子Agent系统
4 种内置 Agent 类型,支持并行执行和递归安全控制。深入 ForkedAgent 的工具过滤策略和进度回调机制。
| Type | 名称 | 权限 | 默认轮次 | 使用场景 |
|---|---|---|---|---|
| general-purpose | 通用 | 读写 | 10 | 子任务孵化(可读写文件) |
| explore | 探索 | 只读 | 8 | 代码探索(搜索/阅读/理解) |
| plan | 规划 | 只读 | 5 | 方案设计(分析/规划/不执行) |
| verify | 验证 | 只读 | 5 | 结果检查(验证/确认/对比) |
ForkedAgent 完整配置
ForkedAgent 的配置接口设计体现了几个重要的工程决策:maxTurns 根据类型设置不同默认值,explore/plan/verify 类型轮次更少,鼓励快速收敛;parentDepth 限制递归深度,防止 Agent 无限嵌套;canUseTool 允许父 Agent 对子 Agent 的工具使用进行精细控制。
onProgress 回调每 30 秒触发一次,向父 Agent 报告子 Agent 的进度摘要。这个机制解决了长时间运行子任务时的「黑盒」问题——父 Agent 可以在等待子任务完成的同时,向用户展示实时进度。
export interface ForkedAgentOptions {
prompt: string; // 子任务描述
llmStream: LLMStreamFn; // LLM 流式调用函数
cwd: string; // 工作目录
maxTurns?: number; // 最大轮次(按类型默认:general=10, explore=8, plan/verify=5)
parentDepth?: number; // 递归深度限制(防止无限嵌套)
agentType?: BuiltinAgentType; // 'general-purpose' | 'explore' | 'plan' | 'verify'
description?: string; // 子任务简述(用于日志和UI展示)
onProgress?: (summary: string, turn: number, toolCount: number) => void;
progressIntervalMs?: number; // 进度回调间隔,默认 30000 (30s)
rawOutput?: boolean; // 是否返回原始输出(不做结论格式化)
canUseTool?: (toolName: string, args: any) => boolean | Promise<boolean>;
}
// 默认工具过滤器 — 防止无限递归
async function defaultCanUseTool(toolName: string): Promise<boolean> {
if (toolName === 'agent_tool') return false; // 禁止子Agent再派生子Agent
if (toolName.startsWith('memory_')) return false; // 禁止修改记忆
// explore/plan/verify 类型只允许只读工具
if (['explore', 'plan', 'verify'].includes(agentType)) {
return isReadOnlyTool(toolName); // 只有搜索、读取类工具
}
return true; // general-purpose 允许所有工具
}Agent 类型元提示词
每种 Agent 类型都有一段精心设计的元提示词(Meta Prompt),注入到子 Agent 的系统提示中。这些提示词约束了子 Agent 的行为边界——只做分配的事、不扩展范围、快速收敛。特别是结论格式要求(结论 + 证据 + 下一步建议),确保父 Agent 能够快速理解子 Agent 的输出并做出下一步决策。
const SHARED_META = `
## 你是子任务执行者
### 通用规则
1. 只做分配的事,不扩展范围
2. 快速收敛,每轮自查"我离目标还有多远"
3. 完成或遇到阻碍后,用结论格式结束
### 结束格式要求
结论: <一句话总结>
证据: <关键发现列表>
下一步: <建议父 Agent 做什么>
`;
// explore 类型额外约束
const EXPLORE_META = SHARED_META + `
### 额外约束
- 只使用只读工具(搜索、读取、查看)
- 不修改任何文件
- 专注于理解和发现信息
- 最多8轮,第6轮起准备收尾
`;
// plan 类型额外约束
const PLAN_META = SHARED_META + `
### 额外约束
- 只使用只读工具进行分析
- 输出结构化的执行方案
- 明确每一步的输入/输出/风险
- 不执行任何实际修改
`;并行执行架构
💡 递归安全控制:parentDepth 限制嵌套深度,防止 Agent 无限递归派生子 Agent。agent_tool 始终从子 Agent 的可用工具列表中过滤,确保子 Agent 不能再派生自己的子 Agent。这是系统级的硬性约束,无法通过配置绕过。这种设计借鉴了操作系统进程树的概念——每个子进程继承父进程的部分权限,但绝不能获得比父进程更高的权限。
守护进程 + Web GUI
不在终端前也能跑:4 层守护架构 + 5 种触发器 + 浏览器双通道控制。深入 daemon 进程管理和 cron 调度器的实现。
守护进程 4 层架构
守护进程让 AI-CLI 能在用户不主动操作时自动执行任务。整个架构分为 4 层:CLI 层是用户交互入口,提供 start/stop/status/trigger 四个命令;IPC 层通过 HTTP 服务(默认端口 3051)实现进程间通信;调度层管理 5 种触发器,根据配置的 cron 表达式、时间间隔、Git 变化或文件变化来触发任务;执行层启动无头模式的 AgentSession(DAEMON_TASK_TURNS = 20),在没有用户交互的情况下运行 Agent。
守护进程的启停使用 PID 文件机制:启动时写入 PID 文件,停止时读取 PID 发送 SIGTERM 信号。进程异常退出时,PID 文件会留下僵尸记录,系统会检查进程是否存活来处理这种情况。
const DAEMON_TASK_TURNS = 20; // 守护任务最多20轮
// 启动守护进程
async function start(config: DaemonConfig) {
// 1. 写入 PID 文件
fs.writeFileSync(PID_PATH, process.pid.toString());
// 2. 初始化 MCP 工具
await mcpManager.initialize(config.mcpServers);
// 3. 启动调度器
const scheduler = new Scheduler(config.triggers);
// 4. 启动 IPC HTTP 服务
const server = createIPCServer(scheduler);
server.listen(3051);
}
// 停止守护进程
async function stop() {
const pid = readPID(); // 读取 PID
if (pid && isProcessAlive(pid)) {
process.kill(pid, 'SIGTERM'); // 优雅停止
await cleanup(pid); // 清理资源
}
}
// 查看状态
function status() {
const pid = readPID();
return {
running: pid && isProcessAlive(pid),
pid,
uptime: getUptime(pid),
pendingTasks: scheduler.getPendingCount(),
};
}Cron 调度器实现
调度器实现了完整的 cron 表达式解析,支持 */N(每N单位)、逗号分隔(1,15)、通配符(*)等标准语法。每分钟检查一次是否有匹配的触发器。getNextCronTime() 函数计算下一次触发时间,用于 UI 展示和延迟调度。
// Cron 表达式解析
function parseCron(cronExpr: string): CronFields {
const [minute, hour, dayOfMonth, month, dayOfWeek] = cronExpr.split(' ');
return {
minute: parseField(minute, 0, 59),
hour: parseField(hour, 0, 23),
dayOfMonth: parseField(dayOfMonth, 1, 31),
month: parseField(month, 1, 12),
dayOfWeek: parseField(dayOfWeek, 0, 6),
};
}
// 匹配当前时间
function matchesCron(cron: CronFields, date: Date): boolean {
return cron.minute.has(date.getMinutes())
&& cron.hour.has(date.getHours())
&& cron.dayOfMonth.has(date.getDate())
&& cron.month.has(date.getMonth() + 1)
&& cron.dayOfWeek.has(date.getDay());
}
// 计算下一次触发时间
function getNextCronTime(cronExpr: string, from: Date): Date {
const cron = parseCron(cronExpr);
let next = new Date(from.getTime() + 60000); // 从下一分钟开始
next.setSeconds(0, 0);
while (!matchesCron(cron, next)) {
next = new Date(next.getTime() + 60000);
}
return next;
}5 种触发器类型
timer (cron)每分钟检查 cron 表达式,支持标准 cron 语法,精确调度
timer (interval)setInterval 定时执行,适合固定间隔的简单场景
git-hook轮询 .git/logs/HEAD 变化,commit/push 时自动触发
file-watchfs.watch + 防抖(500ms),文件变化时触发
manualHTTP POST /trigger 手动触发,可远程触发
Web GUI 架构
💡 实用场景:设置一个 cron 守护任务每天早上9点自动生成 changelog,或者监听 git-hook 在每次 commit 后自动执行代码 review。Web GUI 让你在手机上也能实时查看 AI 的工作进度和执行结果。守护进程 + Web GUI 的组合,本质上是把 AI-CLI 从「被动工具」变成了「主动协作者」——它不需要你下达指令,自己就会在合适的时机执行预设任务。
插件 + Hook 系统
三大开放协议:Plugin API + Tool Hook + External MCP。深入 PluginManager 的加载机制和 Hook 的前后拦截逻辑。
PluginManager:插件加载与隔离
PluginManager 采用独立加载、故障隔离的策略。每个插件独立加载,某个插件加载失败不会影响其他插件的运行。插件通过三个核心 API 与系统集成:registerTool() 注册自定义 MCP 工具,registerCommand() 注册斜杠命令,getConfig() 读取私有配置。这种设计使得插件之间完全解耦,每个插件拥有独立的配置空间和工具命名空间。
export class PluginManager {
private active: Map<string, ActivePlugin> = new Map();
private commands: Map<string, CommandHandler> = new Map();
async loadFromConfig(config: AIConfig): Promise<PluginLoadResult[]> {
const results: PluginLoadResult[] = [];
for (const [name, pluginConfig] of Object.entries(config.plugins ?? {})) {
try {
const plugin = await this.loadPlugin(name, pluginConfig);
this.active.set(name, plugin);
results.push({ name, status: 'loaded' });
} catch (err) {
// 单个插件失败不影响其他插件
results.push({ name, status: 'failed', error: String(err) });
}
}
return results;
}
// 插件 API
registerTool(name: string, tool: ToolDefinition) { ... }
registerCommand(name: string, handler: CommandHandler) { ... }
getConfig(pluginName: string, key: string): unknown { ... }
}registerTool()注册自定义 MCP 工具,支持 Zod Schema 定义参数
registerCommand()注册自定义斜杠命令,拦截并处理用户输入
getConfig()读取插件私有配置,隔离不同插件配置空间
Tool Hook:前后拦截脚本
Tool Hook 是一种声明式的工具增强机制,无需写代码即可在工具执行前后注入自定义逻辑。pre hook 在工具执行前运行,可以修改工具参数(updatedInput)、阻止执行(block)或注入额外上下文(additionalContexts)。post hook 在工具执行后运行,可以修改执行结果、注入上下文或阻止 Agent 继续循环(preventContinuation)。
prePhase 控制前置 hook 的触发时机:beforeConfirm 在安全确认之前(可以阻止危险操作),beforeExecute 在安全确认之后、实际执行之前(确认安全后做最后处理)。
export interface PreToolHookResult {
updatedInput?: Record<string, unknown>; // 修改工具参数
block?: { reason: string }; // 阻止执行
additionalContexts?: string[]; // 注入额外上下文
}
export interface PostToolHookResult {
updatedResult?: Partial<ToolResult>; // 修改执行结果
additionalContexts?: string[]; // 注入额外上下文
preventContinuation?: boolean; // 阻止 Agent 继续循环
}
// 配置示例
export const LIFECYCLE_EVENTS = [
'session_start', 'user_prompt_submit', 'pre_compact', 'post_compact',
'session_end', 'subagent_start', 'subagent_stop',
'post_tool_use_failure', 'stop', 'stop_failure',
] as const;toolHooks:
- tools: ["write_file", "file_system*"] # 通配符匹配
pre: "npx eslint --fix {{file}}" # 写入前自动格式化
post: "npm test -- --changed" # 写入后自动运行测试
prePhase: "beforeConfirm" # 在安全确认前执行
timeout: 15000 # 15秒超时
- tools: ["shell"]
pre: "echo '即将执行: {{command}}'" # 执行前记录日志
post: "echo '执行完成,退出码: {{exitCode}}'"
timeout: 5000生命周期 Hook
lifecycleHooks:
session_start:
- script: "echo '新会话启动' && npx tsc --noEmit"
timeout: 30000
session_end:
- script: "echo '会话结束,清理临时文件'"
stop_failure:
- script: "notify-send 'AI-CLI 任务失败'"
timeout: 5000三大开放协议总结
💡 设计哲学:三大开放协议的设计遵循了 「渐进式扩展」 原则:最简单的需求用 Tool Hook(YAML 配置),中等复杂度用 External MCP(接入第三方),最复杂的用 Plugin API(写代码)。用户不需要为了一个简单的 lint 前置检查去写一个完整的插件——一条 YAML 配置就够了。
三大产品线
ai-cli / cocos-cli / robot-cli:同一核心引擎,三种产品形态,服务不同场景的用户需求。
AI-CLI 并非单一产品,而是基于同一核心引擎(AgentSession + 记忆系统 + 工具系统)构建的三条产品线。三条产品线共享底层架构,但在工具集、Agent 类型和交互模式上有显著差异,分别面向个人开发者、游戏开发团队和自动化运维场景。
| 维度 | ai-cli | cocos-cli | robot-cli |
|---|---|---|---|
| 定位 | 通用 AI 编程助手 | 游戏开发专用 CLI | 无人值守自动化 Agent |
| 目标用户 | 全栈开发者 | Cocos Creator 游戏开发者 | DevOps / SRE 团队 |
| 交互模式 | 终端对话 + Web GUI | 终端对话 + 编辑器集成 | 守护进程 + Web Dashboard |
| 核心工具 | 23 个通用工具 | 通用 + 游戏专用工具 | 通用 + 运维专用工具 |
| Agent 类型 | 4 种内置 | 4 种 + 游戏专用 | 4 种 + 自动化专用 |
| 守护进程 | 可选 | 可选 | 核心功能 |
| 使用场景 | 写代码/调试/重构 | 场景搭建/资源管理/构建部署 | 监控/告警/自动修复 |
ai-cli 是三条产品线中的旗舰产品,面向所有软件开发者。它提供 23 个内置工具,覆盖文件操作、代码执行、搜索、Git 操作等通用场景。4 种内置 Agent 类型(通用、探索、规划、验证)可以组合使用,完成从代码探索到方案设计再到结果验证的完整工作流。
典型使用场景包括:让 AI 阅读代码库并解释架构、自动修复 lint 错误、重构老旧模块、编写单元测试、生成 API 文档等。守护进程是可选功能,适合需要定时执行代码 review 或自动生成 changelog 的团队。
cocos-cli 在 ai-cli 基础上增加了游戏开发专用工具:场景管理工具(创建/修改/预览 Cocos 场景)、资源管线工具(纹理压缩、图集打包、资源引用分析)、构建部署工具(多平台构建、热更新包生成)和调试工具(性能分析、内存泄漏检测)。
它还添加了游戏开发专用的 Agent 类型:SceneBuilder(场景搭建)、AssetOptimizer(资源优化)和 BuildPipeline(构建管线)。这些 Agent 预置了游戏开发的领域知识,能够理解场景层级、组件系统和资源依赖关系。
robot-cli 将守护进程从可选功能提升为核心功能。它专为无人值守场景设计,预置了运维专用工具:服务监控工具(健康检查、日志分析、指标采集)、告警工具(通知分发、升级策略)、自动修复工具(服务重启、配置回滚、扩容缩容)和部署工具(蓝绿部署、金丝雀发布)。
专用的 Agent 类型包括:MonitorAgent(持续监控,异常检测)、FixerAgent(自动诊断和修复)和 DeployAgent(安全部署流程)。robot-cli 的守护进程支持多任务并行,Web Dashboard 提供实时监控视图和告警管理界面。
💡 产品架构哲学:三条产品线的设计遵循了 「核心不变,外围可变」 的原则。AgentSession、记忆系统、安全引擎是三条线共享的不可变核心;工具集、Agent 类型、交互模式是可变的外围。这种架构使得新功能只需在核心层实现一次,三条产品线自动受益。同时,每条产品线的专用工具和 Agent 不会污染核心,保持了系统的简洁和可维护性。
配置体系
config.yaml 完整解读:Provider 配置、MCP 服务器接入、Hook 定义、插件加载。一个 YAML 文件掌控全局。
AI-CLI 的所有配置集中在一个 config.yaml 文件中。这个文件控制着 LLM Provider 选择、MCP 服务器连接、工具 Hook、生命周期 Hook、插件加载等所有行为。配置文件支持项目级(.ai-cli/config.yaml)和用户级(~/.ai-cli/config.yaml)两级,项目级配置覆盖用户级配置。
# ============================================
# AI-CLI 完整配置文件
# ============================================
# ---- Provider 配置 ----
# 指定默认 Provider 和各 Provider 的连接信息
provider: kimi
providers:
kimi:
baseURL: https://api.moonshot.cn/v1
apiKey: sk-xxx
model: kimi-k2
deepseek:
baseURL: https://api.deepseek.com/v1
apiKey: sk-xxx
model: deepseek-chat
openai:
baseURL: https://api.openai.com/v1
apiKey: sk-xxx
model: gpt-4o
# ---- 外部 MCP 服务器 ----
# 通过 stdio 或 SSE 接入第三方工具服务
mcpServers:
filesystem:
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
github:
url: https://api.github.com/mcp/sse
headers:
Authorization: "Bearer ghp_xxx"
dangerLevelOverride:
search_repositories: safe # 搜索自动放行
create_issue: danger # 创建Issue必须确认
# ---- 工具 Hook ----
# 在工具执行前后注入自定义脚本
toolHooks:
- tools: ["write_file", "file_system*"]
pre: "npx eslint --fix {{file}}"
post: "npm test -- --changed"
prePhase: "beforeConfirm" # beforeConfirm | beforeExecute
timeout: 15000
- tools: ["shell"]
pre: "echo '执行: {{command}}' >> /tmp/ai-cli.log"
timeout: 5000
# ---- 生命周期 Hook ----
# 在会话关键节点执行脚本
lifecycleHooks:
session_start:
- script: "echo '新会话启动' && npx tsc --noEmit"
timeout: 30000
session_end:
- script: "echo '会话结束,清理临时文件'"
stop_failure:
- script: "notify-send 'AI-CLI 任务失败'"
timeout: 5000
# ---- 插件 ----
# 加载 npm 包形式的插件
plugins:
my-custom-plugin:
package: "@my-org/ai-cli-plugin"
config:
apiKey: "xxx"
region: "cn"
# ---- Agent 配置 ----
agent:
maxTurns: 50 # 最大循环轮次
compressionThreshold: 80000 # 上下文压缩阈值(tokens)
autoExtract: true # 自动记忆提取
autoDream: true # AutoDream 后台整理
# ---- 守护进程 ----
daemon:
port: 3051 # IPC 端口
tasks:
- name: "每日代码Review"
trigger: "cron"
schedule: "0 9 * * 1-5" # 工作日每天9点
prompt: "审查最近24小时的代码变更"
maxTurns: 20配置项详解
支持多 Provider 切换,每个 Provider 定义 baseURL、apiKey 和 model。通过顶层 provider 字段指定默认使用的 Provider。运行时可通过 /model 命令动态切换。
stdio 模式用 command + args 启动子进程,SSE 模式用 url + headers 连接远程服务。dangerLevelOverride 为每个外部工具独立设置安全级别,覆盖默认分类。
tools 字段支持精确匹配和通配符(*)。pre/post 分别指定前置和后置脚本。{{file}}、{{command}} 等模板变量会在执行时替换为实际参数。prePhase 控制前置 hook 的触发时机。
maxTurns 限制单次会话最大循环轮次。compressionThreshold 设置上下文压缩触发阈值。autoExtract 和 autoDream 控制记忆自动提取和后台整理功能。
💡 配置优先级:配置的优先级从高到低为:命令行参数 > 环境变量 > 项目级 config.yaml > 用户级 config.yaml > 内置默认值。这种分层设计确保了团队协作时的一致性(项目级配置提交到 Git),同时保留了个人偏好的灵活性(用户级配置不提交)。
质量保障
458+ 测试用例、分层测试策略、错误分类体系、重试机制。AI-CLI 如何确保一个会调用 rm -rf 的 AI 系统不会搞砸你的项目?
AI-CLI 是一个会执行 Shell 命令、修改文件系统、操作 Git 仓库的 AI 系统。如果质量保障不到位,后果远不止输出错误文本——它可能删除你的代码、推送错误的 commit、或者泄露敏感信息。因此,AI-CLI 的测试策略不是传统软件的「锦上添花」,而是生死攸关的基础设施。
分层测试策略
AI-CLI 的测试分为 7 个层次,从最底层的纯函数测试到最顶端的端到端集成测试。每层测试有明确的职责边界和运行频率:
| 层次 | 类型 | 测试内容 | 运行频率 |
|---|---|---|---|
| L1 | 单元测试 | 纯函数、工具类、格式化函数 | 每次提交 |
| L2 | 组件测试 | 记忆搜索、上下文构建、配置解析 | 每次提交 |
| L3 | 安全测试 | dangerCheck、AI 分类器、权限边界 | 每次提交 |
| L4 | 集成测试 | 工具执行链、MCP 服务器通信 | 每日 |
| L5 | Agent 测试 | ReAct 循环、偏航检测、错误恢复 | 每日 |
| L6 | 守护进程测试 | cron 调度、IPC 通信、进程管理 | 每周 |
| L7 | 端到端测试 | 完整用户流程(创建项目→编码→测试→部署) | 每周 |
错误分类体系
AI-CLI 将所有错误分为 5 个类别,每个类别有不同的处理策略:Transient(临时错误,自动重试)、Validation(参数错误,返回给 LLM 让其修正)、Permission(权限错误,向用户请求授权)、Resource(资源限制,降级处理)、Fatal(致命错误,终止会话)。
安全关键测试场景
安全测试是最重要的测试层次。AI-CLI 针对以下高危场景编写了专门的测试用例,确保在任何情况下这些安全边界都不会被突破:
验证 AI 分类器始终将 rm -rf 标记为 risky,无论参数格式如何变化
验证 git push、git reset --hard 等不可逆操作必须用户确认
验证子 Agent 不能调用 agent_tool 派生自己的子 Agent
验证子 Agent 不能修改长期记忆(memory_* 工具被过滤)
验证重复工具调用模式被正确检测和限制
验证连续失败8次或空闲10轮时循环被自动终止
重试机制与指数退避
对于 Transient 类别的错误,系统采用指数退避策略自动重试。重试次数、间隔和最大等待时间均可通过配置调整。重试逻辑嵌入在 executeSingleTool() 的 retryTracker 中——每次失败都会记录工具指纹和重试次数,超过最大重试次数后放弃并向 LLM 报告错误。
💡 质量哲学:AI-CLI 的质量保障哲学可以总结为 「安全第一,体验第二」。宁可多弹一次确认框(false positive),也不能让危险操作自动执行(false negative)。458+ 测试用例中,安全测试占了约 30% 的比重,而且每个安全测试都有独立的断言——确保即使其他模块出了 bug,安全边界也不会被突破。这种防御性设计是 AI 系统区别于传统软件的核心特征。
从源码到产品, AI-CLI 的工程哲学
10 个章节,10 个核心模块。每个模块独立运行又紧密协作,共同构成了一个真正能干活的终端 AI 工程师。