Posted on ::

本文为《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 文件获取子智能体的定义(包括 namemodesystemPrompt、允许和禁止的工具列表)。
  • 支持 动态创建:主智能体可以通过 file_write 工具,在运行时的 .aclix/subagents/auto_<name>/ 目录下动态写入新的 SUBAGENT.md。执行完毕后,系统引擎(SubagentManager.cleanupDynamicSubagents)会自动清理这些临时生成的智能体,确保系统无状态残留。

3. 并发调度与读写锁机制

为了防止多个智能体同时修改代码库引发冲突,SubagentManager 实现了一套严格的资源调度锁(acquireSlot)。

  • 并发上限:系统硬编码了最大并发数为 3。超过此限制的调用将被拦截并抛出 SUBAGENT_BUSY 异常。
  • 读写互斥锁:子智能体在元数据中定义了 moderead-onlyread-write)。调度器维护了一个 writerActive 状态位。
  • 当申请 read-write 槽位时,若已有写操作进行中,调用将被拒绝。
  • read-only 模式的子智能体(如代码阅读器、架构规划器)可以并发运行。
  • 通过闭包返回的 release 函数确保任务结束后正确释放槽位和写锁。

二、 终端执行引擎与状态捕获

1. 设计动机

在 Node.js 中通过 execspawn 执行 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 字符串,解析出真实的 cwdenv,并将其更新到常驻内存的 currentCwdcurrentEnv 中。

  • 截断保护:对标准输出和标准错误进行长度限制(最大 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 采用基于文件系统的插件模型,并结合渐进式披露策略。

  • 元数据提取:启动时,SkillManagerRuleManager 通过 fast-glob 遍历内建目录、用户目录及项目目录下的 SKILL.mdRULE.md。通过 gray-matter 解析其 YAML Frontmatter,提取 namedescription
  • 浅层注入:在系统提示词的 ## 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):查询与只读命令。
  • 风险合并:最终的风险等级是 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_writefile_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),显著提高了文件修改的成功率。
Table of Contents