# 01 架构与调用链 对照源码:`D:\ideaProject\QozeCode-main` ## 1. 它到底是什么 QozeCode 是一个**命令行里的 Coding Agent**。用户在终端打字,模型按 ReAct 循环自己决定要不要读文件、跑命令、搜网页;过程实时画在 Textual TUI 上。 它不是 IDE 插件,也不是 Web 对话页。核心就三件事: - 把“对话状态”交给 LangGraph 管 - 把“动手能力”做成 LangChain Tool - 把“思考 / 工具 / 回复”流式渲染到终端 ## 2. 分层(从外到内) ```text ┌─────────────────────────────────────────────────────────┐ │ 启动层 launcher.py + qoze_tui.py:main() │ │ 选模型、读配置、拉起 Textual App │ ├─────────────────────────────────────────────────────────┤ │ 交互层 qoze_tui.py + tui_components/ │ │ 斜杠命令、流式渲染、思考区、工具状态条 │ ├─────────────────────────────────────────────────────────┤ │ 决策层 qoze_code_agent.py │ │ LangGraph: llm_call ⇄ tool_node │ ├─────────────────────────────────────────────────────────┤ │ 模型层 model_initializer.py + config_manager.py │ │ ChatOpenAI 等,兼容 OpenAI 协议的自定义 base_url │ ├─────────────────────────────────────────────────────────┤ │ 能力层 │ │ tools/ 文件、命令、搜索 │ │ skills/ 专家知识注入 prompt │ │ qoze_mcp/ 外部 MCP 工具热加载 │ │ tools/subagent_tool.py 子图并行 │ └─────────────────────────────────────────────────────────┘ ``` ## 3. 目录对照(只记主干) | 路径 | 职责 | 改写时优先级 | |------|------|----------------| | `qoze_tui.py` | Textual App,用户输入入口 | 第二阶段再写,MVP 可用纯 CLI | | `launcher.py` | 启动选模型 | 可做成 `--model` 参数 | | `qoze_code_agent.py` | **心脏**:State、图、checkpoint | 第一阶段必须吃透并重写 | | `model_initializer.py` | 按厂商创建 LLM | 先只留 DeepSeek / OpenAI 兼容 | | `config_manager.py` | 读 ini 配置和密钥 | 先 YAML/ENV 即可 | | `enums.py` | Provider / ModelType | 自己项目用简单字符串就行 | | `utils/system_prompt.py` | 静态 prompt + 动态上下文 | 必须理解,缓存设计值得学 | | `tools/` | 所有 Function Calling 工具 | 先 4 个:读文件、改文件、跑命令、列目录 | | `tools/subagent_tool.py` | 迷你版主图 | 第三阶段 | | `skills/` | SKILL.md 发现与激活 | 第三阶段 | | `qoze_mcp/` | MCP 配置、激活、工具注入 | 第三阶段 | | `tui_components/` | 气泡、流式、thinking widget | 第二阶段 | | `utils/git_context.py` | 把 git status/diff 塞进 prompt | 增强阶段很有用 | ## 4. 启动调用链 ```text qoze / python qoze_tui.py │ ├─ launcher.ensure_config() 没有配置就写模板 ├─ get_model_choice() 交互选模型(可用 --model 跳过) └─ Qoze(provider, model_type).run() │ ├─ initialize_llm() 得到 ChatOpenAI 实例 ├─ llm.bind_tools(tools) ├─ init_agent() SQLite checkpointer 编译 StateGraph └─ 用户回车 └─ process_user_input() ``` TUI 里以 `/` 开头的是**本地命令**,不进图:`/clear`、`/skills`、`/mcp`、`/quit`、`/checkpoint`。 普通自然语言才 `agent.astream(...)`。 ## 5. 一轮对话怎么走完(必须能默画) 主图在 `qoze_code_agent.py`: ```text START │ ▼ llm_call │ 拼 SystemMessage(静态 prompt) │ + 动态上下文(目录树、git、规则、技能) │ + 历史 messages │ → await llm_with_tools.ainvoke(...) │ ├─ 有 tool_calls ──► tool_node ──► 再回到 llm_call │ │ │ └─ 多个 tool_call 用 asyncio.gather 并行 │ └─ 没有 tool_calls ──► END ``` 对应代码: - 状态:`MessagesState`(`messages` 用 `operator.add` 追加) - 节点:`llm_call`、`tool_node` - 边:`should_continue` / `should_continue_from_tool` - 持久化:`AsyncSqliteSaver`,库在当前项目 `.qoze/data/checkpoints.db` 这就是 ReAct:**Reason(llm_call)→ Act(tool_node)→ Observe(ToolMessage 写回)→ 再 Reason**。 ## 6. Prompt 怎么拼(这是质量关键) `llm_call` 每次都会重算上下文,但拆成两段: | 段 | 放哪 | 内容 | 为什么 | |----|------|------|--------| | 静态 | `SystemMessage` | 角色、工具用法、行为规范 | 方便 Prompt Caching | | 动态 | 第一条 `HumanMessage` 前面 | 系统信息、工作目录树、git、`.qoze/rules`、已激活 skill、memory | 每次会变,不能进静态段 | 还要处理两件脏活: - 模型不支持视觉时,剥掉 `image_url` - checkpoint 恢复后,assistant 有 `tool_calls` 但缺 `ToolMessage`,要补占位,否则 OpenAI 兼容接口会 400 ## 7. 工具层真实情况(以代码为准) 当前挂在主 Agent 上的核心工具: - `execute_command`:shell(Windows 也能跑,但没有 Linux 的进程组杀法) - `read_file` / `list_files` / `search_in_files` / `grep_file` / `find_files` / `replace_in_file` - `tavily_search` / `read_url` - skill 四个:activate / list / deactivate / install guide - MCP 四个:list / activate / deactivate / install guide - `dispatch_subagent` - `analyze_project` / `find_symbols` / `trace_imports` - `transcribe_audio` / `get_current_datetime` 注意: - `write_file` **被注释了**,写文件靠 `replace_in_file` 或 `execute_command` - `browser_*` **整段注释**,浏览器走 MCP(`chrome-devtools`) - README 的 `/plan` 在这份代码里**没有对应实现**,别按文档去找 路径安全:`file_tools._resolve_under_cwd` 只允许 cwd 和 `~/.qoze/`,这是改写时必须保留的约束。 ## 8. 三条插件通道(先分清,再决定要不要抄) ```text Skill 发现 SKILL.md → 用户 activate → 把文档塞进动态 prompt MCP 读 mcp_config.json → 拉起外部进程 → 把远端 tools 注入 tools_by_name Subagent 再建一张同样的小图,工具更少、无 checkpoint、上下文隔离 ``` 记忆口诀: - Skill = **知识**(改 prompt) - MCP = **外来工具**(改 tool 列表) - Subagent = **再开一个大脑**(改执行拓扑) ## 9. TUI 流式 `tui_components/messages/stream_handler.py` 订阅 `agent.astream`: - 模型 `reasoning_content` → `ThinkingWidget` - 正文 token → `BotMessageWidget`(增量 buffer + 调度器,避免刷爆终端) - 出现 tool_call → 工具状态面板 - ToolMessage 回来 → 标记完成/失败 自己复刻时,第一版可以不流式,等图跑完再 `print`。流式是体验,不是 Agent 正确性。 ## 10. 和宣传文案不一致的地方(精读避坑) | README 说法 | 这份源码 | |-------------|----------| | `/plan` 三份文档 | 未见实现 | | Playwright 浏览器工具 | `browser_tool.py` 注释,改 MCP | | 语音 | 有 `AudioManager` / Soniox,Windows 基本不可用 | | 内置 skills 一堆 | 仓库 `skills/` 只有管理器,内置技能目录可能不在这份拷贝里 | 精读原则:**调用链以 `qoze_code_agent.py` + `qoze_tui.py` 为准。**