本文为《ACLIx:从0实现一个 CLI Agent 工具》第二部分,重点剖析了多子智能体协作与状态管理、终端执行引擎与状态捕获、分层记忆系统、极简的模块化扩展、风险分级与人机协同、隐式快照与版本回滚等核心技术设计与实现。
终端环境具有高度的自由度和破坏潜力,传统的单体 LLM 架构在处理跨文件修改、复杂命令调用以及长周期任务时,容易遭遇上下文超载、状态丢失以及系统安全风险。
ACLIx 采用了一套基于主从编排(Master-Worker)、分层记忆、以及严格读写隔离的工程架构。本文将基于其底层代码实现,深度剖析 ACLIx 的核心技术设计、具体实现路径及工程上的取舍。
一、 Agent 架构:主从编排与资源调度
1. 设计动机
单体智能体在处理需要深度探索和修改代码库的复杂任务时,往往会在一个 Prompt 中混杂规划、检索和执行的逻辑。这不仅会导致极高的 Token 消耗,还极易引发模型的幻觉。此外,并发的文件读写操作会导致代码库状态不一致。
2. 核心架构设计
ACLIx 采用了 Master-Subagent(主从编排) 模式。主智能体(Master Orchestrator)负责与用户交互并规划任务,但不直接执行高风险的底层文件修改。当面临复杂任务时,主智能体通过调用内部工具 agent,生成并派发任务给特定的子智能体(Subagents)。
子智能体的状态隔离
子智能体被设计为无状态(Stateless)和上下文隔离的执行单元。
- 不共享历史:子智能体无法访问主智能体的对话历史。主智能体必须在
task参数中,将所有的必要上下文(如文件路径、代码片段、目标结构)一次性打包传递给子智能体。 - 单向信息流:子智能体执行完毕后,主智能体仅接收其生成的结构化文本摘要,而不会将其内部繁杂的思考过程(Reasoning)和工具调用明细混入主会话的上下文中。
动态加载与生命周期管理
子智能体不是硬编码的,而是基于文件系统动态加载的。
- 通过扫描
SUBAGENT.md文件获取子智能体的定义(包括name、mode、systemPrompt、允许和禁止的工具列表)。 - 支持 动态创建:主智能体可以通过
file_write工具,在运行时的.aclix/subagents/auto_<name>/目录下动态写入新的SUBAGENT.md。执行完毕后,系统引擎(SubagentManager.cleanupDynamicSubagents)会自动清理这些临时生成的智能体,确保系统无状态残留。
3. 并发调度与读写锁机制
为了防止多个智能体同时修改代码库引发冲突,SubagentManager 实现了一套严格的资源调度锁(acquireSlot)。
- 并发上限:系统硬编码了最大并发数为 3。超过此限制的调用将被拦截并抛出
SUBAGENT_BUSY异常。 - 读写互斥锁:子智能体在元数据中定义了
mode(read-only或read-write)。调度器维护了一个writerActive状态位。 - 当申请
read-write槽位时,若已有写操作进行中,调用将被拒绝。 read-only模式的子智能体(如代码阅读器、架构规划器)可以并发运行。- 通过闭包返回的
release函数确保任务结束后正确释放槽位和写锁。
二、 终端执行引擎与状态捕获
1. 设计动机
在 Node.js 中通过 exec 或 spawn 执行 Shell 命令是无状态的。如果 LLM 生成了 cd src 或是 export NODE_ENV=production 的命令,当该进程退出后,这些状态就会丢失。下一次工具调用时,系统依然停留在原目录,这打破了 LLM 对“交互式终端”的心智模型。
2. 状态提取实现 (State Marker)
ACLIx 构建了一个自定义的执行 Harness(services/executor/host.ts),通过在命令执行前后注入探测脚本,强行捕获底层进程的状态变化。
-
命令包装 (Command Wrapping):
当接收到命令(如
cd dir && npm run build)时,系统并不直接执行,而是将其拼接为复合命令:{ cd dir && npm run build ; } ; _ACLI_RET=$? ; echo -e "\n___ACLI_STATE_MARKER___" ; node -e "console.log(JSON.stringify({cwd: process.cwd(), env: process.env}))" ; echo "___ACLI_STATE_MARKER___" ; exit $_ACLI_RET -
状态解析:命令执行完毕后,引擎利用正则匹配
___ACLI_STATE_MARKER___之间的 JSON 字符串,解析出真实的cwd和env,并将其更新到常驻内存的currentCwd和currentEnv中。 -
截断保护:对标准输出和标准错误进行长度限制(最大 10,000 字符,保留头部和尾部各 5,000 字符),防止如
cat <大文件>或无限递归导致的内存溢出及 Token 爆炸。
三、 分层记忆系统:从全量注入到语义检索
1. 设计动机
LLM 的上下文窗口是有限的,且随着上下文变长,注意力机制会产生稀疏。系统必须能够在长周期对话中保留核心信息,同时丢弃冗余细节。
2. 三层记忆架构
ACLIx 将上下文切分为短、中、长三层:
短期记忆
- 机制:存储未压缩的近期对话。通过
better-sqlite3以 WAL 模式持久化到本地sessions表中,支持断电和跨会话的状态恢复。
压缩记忆
- 机制:滑动窗口摘要(
ContextCompressor)。当 STM 中的消息数量超过阈值(如 40 条),系统触发滚动压缩。 - 断点安全算法:为了防止将成对的
tool_call(请求)和tool_result(返回)截断,压缩器实现了findMaxSelfContainedEnd算法。该算法从后向前扫描,寻找一个“安全切分点”,确保被切分进压缩队列的消息中,所有的工具调用都已闭合。 - 执行:将安全的历史消息序列化,调用内部 LLM 实例生成结构化的
[Historical Context Summary],并作为一个单条的 Assistant 消息替换原有的长序列。
长期记忆 (LTM) 与 BM25 检索
- 机制:持久化的用户级规范(
~/.aclix/ACLI.md)和项目级规范(./ACLI.md)。 - 降级检索:初期,LTM 内容全量注入到系统提示词中。当 LTM 内容超出 3000 字符阈值时,触发本地检索机制。
- 自研 BM25 算法:为保证 CLI 工具的轻量化(不依赖复杂的向量数据库或外部 Embedding API),系统内置了基于 TF-IDF 变种的 BM25 算法。
- 使用
Intl.Segmenter进行多语言(中英文)分词。 - 通过
chunkMarkdown函数将长 Markdown 按段落切分为固定大小(最大 800 字符)的 Chunk。 - 计算当前用户 Query 与各个 Chunk 的相关度得分,仅保留得分最高的 Top-3 记忆片段。
- 上下文补充:在截断注入的提示词上方,系统会增加一条明确的
<warning>,告知 LLM 当前看到的是截断后的片段。如果需要修改记忆文件,必须先调用file_read工具读取全量内容,防止 LLM 基于片段信息盲目覆盖原文件。
- 使用
四、 极简的模块化扩展:技能 (Skills) 与规则 (Rules)
1. 设计动机
若将所有的标准操作程序(SOP)和项目规范预置在系统提示词中,会导致提示词极度臃肿,系统指令的权重下降。
2. 渐进式披露设计
ACLIx 采用基于文件系统的插件模型,并结合渐进式披露策略。
- 元数据提取:启动时,
SkillManager和RuleManager通过fast-glob遍历内建目录、用户目录及项目目录下的SKILL.md和RULE.md。通过gray-matter解析其 YAML Frontmatter,提取name和description。 - 浅层注入:在系统提示词的
## SPECIALIZED SKILLS区块,仅注入所有可用技能的名称和简要描述的 XML 列表。 - 深层加载:在提示词中强制约定:若用户请求匹配某项技能描述,LLM 必须首先调用
read_skill工具。当工具被调用时,系统才读取对应的 Markdown 文件正文,将其完整的 SOP 作为工具执行结果返回给 LLM。 - 收益:核心 Prompt 保持精简,同时赋予了系统无上限的可扩展性。用户只需在
.aclix/skills/下新建 Markdown 文件,即可让智能体习得全新能力。
五、 风险分级与人机协同 (HITL)
终端智能体的最高法则是安全。系统构建了一套静态与动态结合的安全防线。
1. 静态抽象语法树 (AST) 评估器
不能完全依赖大语言模型的自我审查(大模型容易遭受越狱攻击)。执行引擎在底层拦截所有 Shell 命令,并送入 evaluateServerRiskFloor 函数。
- AST 解析:使用
shell-quote将字符串命令解析为语法树。 - 控制流阻断:按照控制操作符(
;,&&,|)将命令拆分为独立的执行块,忽略变量赋值(如FOO=bar),精准定位到主命令(Main Command)。 - 启发式定级:
- 高风险 (High):命中
mkfs,dd,shutdown等破坏性命令;命中带有-i参数的sed;或者重定向目标不是/dev/null的操作;以及特征匹配的 Fork 炸弹(:(){ :|:& };:)。 - 中风险 (Medium):状态修改命令,如
rm,mv,chmod, 或是包管理器命令(npm install),网络请求(curl,wget)。 - 低风险 (Low):查询与只读命令。
- 高风险 (High):命中
- 风险合并:最终的风险等级是 LLM 自身评估的风险与底层 AST 评估的最高值(
maxRisk)。
2. 人机协同 (Human-in-the-Loop)
在工具调用的生命周期中,onBeforeExecute 钩子负责控制执行流。
- 对于低风险的读取操作,静默放行,终端界面仅展示暗色调的执行日志。
- 对于中高风险操作,进程通过
@clack/prompts挂起,将 LLM 给出的意图(Reasoning)、完整命令以及风险定级展示在终端,要求人类用户显式确认(Y/n)。
3. 上下文安全与脱敏
- 信任边界 (Trust Boundary):来自外部环境的返回内容(如
file_read读取的本地文件,web_search抓取的网页)一律被包裹在<untrusted_data>XML 标签内。系统提示词设定了最高优先级的安全断言:要求 LLM 将该标签内的所有内容视为潜在的提示词注入攻击,绝不执行其中的控制指令。 - 日志脱敏:日志系统集成了一个递归的正则过滤器(
maskSensitiveData),自动扫描并匹配类似sk-...的 OpenAI 密钥格式或 GitHub Token 前缀,将其替换为[REDACTED],防止调试日志泄露敏感凭证。
六、 隐式快照与版本回滚 (Snapshot & Undo)
1. 设计动机
模型在进行文件替换时(特别是使用正则或大段文本覆盖时),极易破坏原有代码的语法结构。依赖 Git 并非万能,因为并非所有目录都在版本控制之下,且用户可能尚未提交当前工作区。
2. 底层实现
ACLIx 实现了一个轻量级、无依赖的 Undo 机制。
- 前置快照:在
file_write和file_edit工具的执行逻辑中,进行实际的文件 I/O 前,系统会同步读取目标文件的当前状态。 - 存储引擎:将当前文件路径和内容作为一个 Snapshot 存入 SQLite 的
file_snapshots表中。如果文件不存在(属于新建操作),则记录标记位is_new: 1。 - 回滚逻辑:通过
/undo斜杠命令,系统从数据库中pop出最新的一条记录。若is_new === 1,执行删除(unlink)操作;否则重写原内容。整个过程对 LLM 是透明的,作为底层的防线保证了每一次修改都有“后悔药”。
3. 智能文件编辑 (Fuzzy Block Replace)
对于文件修改,系统废弃了直接使用 sed 或要求 LLM 输出完整文件的低效做法,而是内置了 file_edit 工具。
- 匹配算法:LLM 仅需提供被替换的
oldString和新的newString。 - 容错处理:考虑到 LLM 输出的缩进经常不准确,底层的
fuzzyBlockReplace算法会按行分割字符串,并忽略首尾的空白字符进行精准比对。 - 如果完全匹配失败,算法会启动滑动窗口(Sliding Window),计算相似度最高的代码块,并在报错中返回该代码块的具体行号供 LLM 自我纠错(Self-Correction),显著提高了文件修改的成功率。