|
|
# 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` 为准。**
|