Files
blackbird/docs/reports/aibox_cli_once_mode_dev_doc.md
T
qing ce142fe32f docs(aibox): sync once-mode docs (MODULES/ARCHITECTURE/dev-doc)
- MODULES.md/ARCHITECTURE.md:aibox 条目补 --once CLI 与 models 多模型档案(三处同步惯例)
- 开发文档新增第 7 节实施状态:四批提交映射、与计划的 4 处偏差(--once-timeout 改名/models 默认空/worker-only 精简 prompt/退出码 1 边界)、验证结果
- 原 7/8 节顺延为 8/9
2026-08-17 17:12:15 +08:00

18 KiB
Raw Blame History

aibox CLI 单次调用模式 · 开发文档

文档类型:功能开发计划 模块:aibox(单 AI 智能体系统) 日期:2026-08-17 状态:方案研究(待评审) 影响范围:aibox/aibox.py(仅增量改动,WebSocket 模式零回归)


1. 概述

1.1 背景与动机

aibox 目前只有两种使用方式:

使用方式 入口 特点
WebSocket 服务器 python aibox.py → ws://localhost:8765 长连接、多轮对话、前端/BotOps 交互(核心场景)
备忘录 CLI --check-memo / --memo-add / --memo-list / --memo-complete 只做任务 CRUD,不触发 AI 对话

缺少一种**"问一次就退"**的 CLI 一次性对话模式。这导致以下场景无法覆盖:

  1. 脚本化调用:无法在 shell 脚本 / cron / CI 流水线中把 AI 当命令行工具用(如 aibox --once "统计当前目录 py 文件行数" | grep ...)
  2. 资源受限环境:树莓派 / NPU 盒子上不想常驻一个 WebSocket 服务进程,只想按需拉起、用完即退
  3. 调试与测试:复现某个 prompt 行为必须启动服务器 + 前端连接,链路太重
  4. 进程级隔离执行:Worker 子代理当前是同进程线程(run_full_subagent → DeepSeekChat),子代理崩溃/死循环会影响主服务;有了 CLI 单次模式后,可演进为"独立子进程执行",OS 级隔离

1.2 设计目标

  1. 新增 --once QUERY 参数:执行一次性 AI 对话(含完整工具调用链),结果流式输出到 stdout,完成后进程退出
  2. 纯增量、零回归:不改动 WebSocket 模式任何现有行为;不加参数时行为与现状完全一致
  3. 复用现有架构:复用 EnhancedAIManager / DeepSeekChat / Orchestrator-Worker 工具分层 / 三层中断机制,不重写对话逻辑
  4. 管道友好:stdout 只出 AI 回答正文,过程行(🔧 工具调用等)走 stderr,退出码语义化,可被 shell 脚本安全组合
  5. 状态隔离明确:单次模式默认不加载历史对话、不写对话归档,记忆写入显式可控

1.3 非目标

  • 不做多轮对话 CLI(交互式 REPL 已由 client.py 覆盖)
  • 不做 HTTP/REST 单次接口(如需可在后续批次评估,复用同一内核)
  • 不改变 Worker 子代理的线程内执行方式(进程化是后续可选演进,不在本期范围)

2. 现状分析

2.1 入口结构(aibox.py)

parse_args()                      # L2786: argparse 定义
main()                            # L2927: 主入口
  ├── 语言/配置/调试覆盖          # L2931-2959
  ├── 目录创建 + 日志初始化       # L2961-2973
  ├── SubprocessCommandExecutor   # L2976
  ├── EnhancedAIManager           # L2978-2981
  ├── handle_check_commands()     # L2983: --check-memo 处理后 return
  ├── handle_memo_commands()      # L2986: --memo-* 处理后 return
  └── manager.run()               # L2990: WebSocket 服务器(唯一终点)

关键观察:

  • handle_check_commands / handle_memo_commands 已经是**"处理后即退出"的 CLI 模式先例**,单次对话模式可完全沿用该模式:在 manager.run() 之前插入 handle_once_mode(manager),命中则执行并 return
  • EnhancedAIManager.add_ai()(L2584)已封装完整的 AI 实例组装(memory_db / memo_db / history_manager / prompt_manager / command_executor 注入),单次模式可直接复用
  • DeepSeekChat.think_and_respond(input_text, output_callback)(L1308)是同步阻塞函数,天然适合 CLI 直接调用——不需要 asyncio 事件循环

2.2 output_callback 消息类型(流式输出契约)

think_and_respond 通过 output_callback(msg_type, content) 实时推送:

msg_type 语义 CLI 模式去向
chunk AI 回答增量片段(权威内容来源) stdout(流式 write + flush)
complete 收尾信号(内容与 chunk 累积重复) 仅作收尾标记,不重复输出
reasoning_start/reasoning/reasoning_end 思考模式流式 stderr(可用 --show-reasoning 控制)
line 过程行:🔧 工具调用、工具输出、💬 前缀 stderr
ai_break 工具调用分段信号 忽略(CLI 无 widget 概念)
error API/解析错误 stderr + 退出码 2
dialogue_ended AI 主动结束对话 正常收尾

2.3 需要处理的技术要点

要点 现状 风险与对策
压缩输出污染 _think_and_respond_impl L1346 中 print("📦 对话压缩...") 直接打 stdout 单次模式默认不加载历史 → 消息极少 → 几乎不触发压缩;另需确认该 print 走 stderr 或抑制(Batch C 处理)
记忆写入并发 多个 CLI 进程并发写 memories.db(SQLite) 默认 --once 模式 AI 侧仍可调用 memory 工具(orchestrator profile);SQLite 有文件锁兜底,低频并发无碍;提供 --no-save-memory 开关时通过提示词引导(见 §3.4)
Ctrl+C 中断 三层取消机制(Event + HTTP close + 子进程 kill)依赖 cancel() 被调用 CLI 模式注册 signal.SIGINT handler 调 manager.cancel(),与 WebSocket handle_interrupt 路径等价
子代理输出转发 SubAgentTool._subagent_output 经 send_line → output_queue → 回调链 CLI 模式的 output_callback 会自然收到子代理的 line/reasoning,全部导流 stderr,无需特殊处理
对话归档写入 global_exit_handler 会调 history_manager.end_current_conversation() 单次模式传 exit_callback=None(与 TaskScheduler 的任务实例一致),AI 主动 save_memory_and_end_conversation 时不触发全局归档

3. 设计方案

3.1 命令行接口

# 基本用法:一次性对话,结果输出到 stdout
python aibox.py --once "帮我统计当前目录下所有 py 文件的行数"

# 静默模式:只输出最终回答正文(工具过程完全静默,适合管道)
python aibox.py --once "把 README.md 翻译成英文摘要" --quiet

# 显式控制
python aibox.py --once "..." --no-save-memory     # 不写入记忆数据库
python aibox.py --once "..." --timeout 120         # 全局超时 120 秒(默认 300)
python aibox.py --once "..." --show-reasoning      # stderr 显示思考过程(默认不显示)

新增参数(全部可选,--once 之外均有默认值):

参数 类型 默认 说明
--once QUERY str — 一次性对话模式:执行 QUERY 后退出,不启动 WebSocket
--quiet flag false 仅 stdout 输出回答正文;过程行/thinking 全静默
--no-save-memory flag false 禁止本次对话写入记忆数据库
--timeout SEC int 300 全局执行超时,超时 kill 并退出码 4
--show-reasoning flag false 在 stderr 显示思考过程流

3.2 退出码语义

退出码 含义
0 成功(AI 正常完成回答)
1 工具执行链路异常(如 run_command 失败且 AI 报告失败)
2 API 请求失败(网络/HTTP/JSON 解析,服务端已回滚消息)
3 用户中断(Ctrl+C)
4 全局超时(--timeout 到期)

Batch A 只保证 0 / 2 / 3 / 4;退出码 1 的判定(从 AI 文本中识别失败)在 Batch B 实现,初版可统一归 0。

3.3 核心实现(Batch A,约 60~80 行增量)

改动点 1:parse_args()(aibox.py L2786) 追加 §3.1 的 5 个参数定义。

改动点 2:新增 handle_once_mode(),插入 main() 中 handle_memo_commands() 之后、manager.run() 之前:

def handle_once_mode(manager, args) -> bool:
    """一次性对话模式:命中 --once 则执行并返回 True(进程退出)。"""
    if not args.once:
        return False

    ai = manager.add_ai()
    ai.history_manager = None          # 单次模式不加载/不写对话历史
    if args.no_save_memory:
        ai.memory_db = None            # 工具注册时已注入;此处断引用防止运行期写入
        # 注:Batch A 中 memory 工具已在 _register_tools 注册;
        # 完整禁用见 §3.4 说明

    exit_code = [0]

    def cli_callback(msg_type, content):
        if msg_type == "chunk":
            sys.stdout.write(content)
            sys.stdout.flush()
        elif msg_type == "complete":
            sys.stdout.write("\n")
            sys.stdout.flush()
        elif msg_type == "error":
            print(content, file=sys.stderr)
            exit_code[0] = 2
        elif msg_type == "line" and not args.quiet:
            sys.stderr.write(content)
            sys.stderr.flush()
        elif msg_type.startswith("reasoning") and args.show_reasoning:
            sys.stderr.write(content)
            sys.stderr.flush()
        # ai_break / dialogue_ended 忽略

    # Ctrl+C → 三层取消(与 WebSocket interrupt 路径等价)
    def _sigint(sig, frame):
        manager.cancel()
    signal.signal(signal.SIGINT, _sigint)

    # 全局超时:守护线程到点 cancel
    timer = threading.Timer(args.timeout, manager.cancel)
    timer.daemon = True
    timer.start()
    try:
        response = ai.think_and_respond(args.once, cli_callback)
        if response is None or (isinstance(response, str)
                                and response.startswith(f"\n{i18n.get('error_prefix')}")):
            exit_code[0] = max(exit_code[0], 2)
        # 被 cancel 打断时 think_and_respond 返回 interrupt 文案
        if ai._is_cancelled() and timer.is_alive() is False:
            exit_code[0] = 4
    finally:
        timer.cancel()
        ai.cancel()  # 兜底清理子进程

    sys.exit(exit_code[0])

改动点 3:main() 一行插入:

    if handle_memo_commands(manager):
        return

    if handle_once_mode(manager, args):   # 新增
        return

    manager.run()

注:handle_once_mode 内部 sys.exit(),return 分支实际不可达,保留 return 仅为与 handle_check_commands / handle_memo_commands 模式一致。

3.4 记忆写入的处理

--no-save-memory 的完整实现需要在工具注册层禁用 MemoryTool,但 _register_tools 依赖构造期注入的 memory_db。Batch A 采用轻量方案:

  • Batch A:仅断 ai.memory_db = None 引用 + 文档声明"--once 场景不建议依赖记忆";AI 若调用 memory 工具会因注入的 db 为 None 由工具自身报错兜底(MemoryTool.execute 内部访问 db 返回错误字符串,不会崩溃)
  • Batch B:DeepSeekChat.__init__ 增加 enable_memory: bool = True 形参,为 False 时 _register_tools 跳过 MemoryTool 注册,工具 schema 不出现在请求中,LLM 无从调用——这才是干净方案

3.5 与既有架构约束的对齐

项目硬约束 本方案如何满足
WebSocket localhost:8765 通信 不受影响;--once 不启动 WebSocket,互斥使用
Worker 仅 7 种执行工具 不受影响;单次模式沿用 orchestrator profile,工具分层不变
工具输出 ≤1MB + 超时中断 不受影响;复用 SubprocessCommandExecutor
单向依赖 common←subsystems←tools←aibox 满足;改动全部在 aibox.py 聚合层
新增/变更模块三处同步(根README/docs/MODULES/ARCHITECTURE) 本功能非新模块,但落地时需同步更新 aibox/README.md 的"命令行参数"章节

4. 分批次实施计划

遵循项目"增量重构、低风险先行"惯例:

Batch A:最小可用版(核心,建议先做)

  • parse_args() 新增 --once / --quiet / --timeout 三个参数
  • 新增 handle_once_mode()(§3.3 实现)
  • main() 插入调用点
  • SIGINT handler + 全局超时 Timer
  • 退出码 0/2/3/4
  • 验收:python aibox.py --once "现在几点了" 正常输出并退出;echo $? 为 0;WebSocket 模式回归(启动服务器 + client.py 对话一轮)

Batch B:体验与边界补齐

  • --no-save-memory / --show-reasoning 参数落地
  • DeepSeekChat.__init__ 增加 enable_memory 开关,干净禁用 MemoryTool
  • _think_and_respond_impl L1346 压缩提示 print 改走 stderr(或注入 print 回调)
  • 退出码 1(工具失败识别)
  • 补充 tests/test_once_mode.py:mock LLM 客户端,验证 stdout/stderr 分流、退出码、超时路径
  • 更新 aibox/README.md 命令行参数章节

Batch C:安全加固(可选,按需)

  • SQLite WAL 模式(PRAGMA journal_mode=WAL),缓解多 CLI 进程并发写锁等待
  • --worker-only 开关:tool_profile=worker 纯执行面模式,供其他模块以子进程方式调用 AI 执行器(为"Worker 进程级隔离"演进铺路)
  • --model / --temperature 覆盖参数

明确不做(本期)

  • 多轮交互式 CLI REPL(client.py 已覆盖)
  • HTTP 单次接口
  • Worker 子进程化(架构演进议题,单独立项)

5. 风险清单

风险 等级 缓解
冷启动开销(PromptManager 加载 + SQLite 初始化 + LLM 握手 ≈1~3s) 低 可接受取舍;批量场景文档引导用 WebSocket 长连接
CLI 模式与 WebSocket 模式代码 drift 中 复用 EnhancedAIManager.add_ai(),单次模式新增代码控制在 80 行内,核心对话逻辑零复制
print 污染 stdout 破坏管道 中 Batch A 用 --quiet 兜底;Batch B 根治压缩提示 print
并发进程 SQLite 写锁 低 低频无碍;Batch C WAL 加固
Ctrl+C 时子进程泄漏 低 复用现成三层取消;finally 中 ai.cancel() 兜底

6. 测试方案

6.1 单元测试(tests/test_once_mode.py,Batch B)

用例 验证点
正常问答 mock LLM 返回纯文本 → stdout 含回答、退出码 0
工具调用链 mock LLM 返回 tool_calls → line 走 stderr、chunk 走 stdout
API 失败 mock LLM 抛 RequestException → 退出码 2、stderr 含错误
超时 mock LLM 阻塞 → Timer 触发 cancel → 退出码 4
SIGINT 发送 SIGINT → 三层取消触发 → 退出码 3
--quiet 过程行不输出到任何流
--no-save-memory memories.db 无新增行

6.2 回归测试

  • py -m pytest aibox/tests -q 现有 232 项全过
  • WebSocket 模式手测:启动服务器 + client.py 完成一轮对话 + Ctrl+C 中断

6.3 手动验收脚本

# 基础问答(无工具)
python aibox.py --once "用一句话介绍你自己" ; echo "exit=$?"

# 工具调用链(会派生 worker 子代理执行命令)
python aibox.py --once "在 ~/.aibox/workspace 下创建一个 hello.txt 写入 hello" ; echo "exit=$?"

# 管道组合
python aibox.py --once --quiet "列出当前目录 py 文件" | grep ".py" | wc -l

# 超时路径
python aibox.py --once --timeout 5 "运行 sleep 60" ; echo "exit=$?"   # 期望 4

7. 实施状态(2026-08-17 落地记录)

本节为实施后回填,与第 3/4 节计划存在少量偏差,以本节为准。

7.1 提交与批次

批次 提交 内容
Batch A ce3fb7f --once / --quiet / --once-timeout(300s 默认)+ handle_once_mode + SIGINT/超时复用三层取消
Batch B 9e0d39c --no-save-memory(enable_memory 构造开关干净禁用 MemoryTool)、--show-reasoning(思考流默认静默走 stderr)、压缩提示改走 output_callback、退出码 1(orchestrator 层工具异常)+ test_once_mode.py 10 项
Batch C a5af4bb SQLite WAL、--worker-only(worker profile + 精简 system prompt 防人格污染)、--model / --temperature、子代理失败标记透传(_SUBAGENT_TOOL_FAILED_MARKER → orchestrator stats.tool_failed)+ 5 项测试
Batch D d03dbc7 多模型档案 models 配置节(本档案为实施中新增需求,超出原计划范围)+ test_models_profile.py 17 项 + README 更新

7.2 与计划的偏差

  1. 超时参数名:计划 --timeout,落地为 --once-timeout(避免与 api.timeout 语义混淆)
  2. models 默认留空 {}:计划未涉及;实施中发现默认档案硬编码 DEEPSEEK_API_KEY / 官方 URL 会覆盖用户自定义配置(本机为 ARK_API_KEY + 火山方舟端点,曾导致 401),故默认空 dict,未配置档案时零破坏回退旧 api 扁平节
  3. --worker-only 需要精简 system prompt:原计划仅切 tool_profile;实测 orchestrator 的人格/身份提示词会污染 worker 语义(自称"老板"、主动写文件、多轮不收敛),已对齐 run_full_subagent 的子代理语义修复
  4. 退出码 1 的边界:仅覆盖 orchestrator 直接感知的工具异常;worker 子代理内的命令失败经 Batch C 的标记透传后同样计入。子命令失败但 AI 正常汇报时退出码为 0(AI 视角任务完成)

7.3 验证结果

  • 单测 264 项全过(once 模式 15 + 多模型档案 17 新增)
  • 冒烟:基础问答 exit 0 / 工具调用链(子代理执行 echo)exit 0 / 超时路径 exit 4 / --no-save-memory --quiet stdout 干净 / --show-reasoning 思考流走 stderr / --temperature 透传 / 不存在的模型名 exit 2

7.4 明确不做(维持原判)

WebSocket 会话恢复、--once 结果缓存、批处理多 query——见第 4 节"明确不做"。


8. 后续演进方向(不在本期)

  1. Worker 进程级隔离:run_full_subagent 的线程内 DeepSeekChat → subprocess.run(["python", "aibox.py", "--once", "--worker-only", ...]),子代理崩溃不波及主服务,可用 OS 用户/chroot 做权限隔离
  2. HTTP 单次接口:POST /chat 无状态接口,与 --once 共享内核
  3. 结构化输出:--json 输出 {answer, tool_calls, exit_reason} JSON,供程序消费

9. 决策记录

决策点 结论 理由
新入口文件还是复用 aibox.py 复用 aibox.py + --once 参数 沿用 --check-memo 先例;避免维护两份入口 drift
stdout 输出协议 chunk 流式 + 末尾换行 与 WebSocket 的 chunk 权威语义一致;管道可逐段消费
过程行去向 stderr Unix 管道惯例:stdout 是数据、stderr 是元信息
单次模式是否加载历史 否 "一次性"语义明确;避免历史污染导致行为不可复现
是否默认写记忆 默认与 orchestrator 一致(可写),--no-save-memory 显式关闭 与 WebSocket 模式行为对齐,减少心智负担