Skip to content

25|项目骨架:目录怎么长出来

前四阶段建立了全部设计决策。最后这一阶段把它们串进一个能跑的 ops-assistant。

本篇讲项目骨架。目录不是一开始就设计好的,是随着设计决策增加演化出来的。每个模块对应前面某一篇的设计决策,加一个决策就长一个模块。

从最小目录开始

第一篇跑通调用时,目录是这样的:

text
ops-assistant/
├── config.py          # 第 2 篇:settings(含 base_url)
├── scripts/
│   └── check_alert.py
├── data/
│   └── alerts/
├── prompts/           # 第 5 篇:Prompt 模板
├── outputs/
├── pyproject.toml
└── .env.example

这时候没有 agent,没有循环,就是一个调模型的脚本。够用。不要一上来就建一堆空目录。

随着决策长模块

第二阶段加了会话状态和上下文,长出 agent/ 模块:

AIOps 概念图:目录随决策生长

text
├── agent/
│   ├── engine.py      # 第 12 篇:QueryEngine + queryLoop
│   ├── context.py     # 第 9 篇:上下文组装管线
│   └── compact.py     # 第 10 篇:压缩策略

第三阶段加了循环控制,相关逻辑融进 agent/engine.py——错误处理、停止条件、继续工作、预处理都是 queryLoop 的一部分,不单独建模块。流式并发是工具执行的事,放 tools/

第四阶段加了工具系统和权限,长出 tools/harness/

text
├── tools/
│   ├── registry.py    # 第 18 篇:工具注册中心
│   ├── read_log.py    # 第 21 篇:只读工具
│   ├── file_edit.py   # 第 21 篇:写操作工具
│   └── bash.py        # 第 20 篇:BashTool
├── harness/
│   ├── permissions.py # 第 19 篇:权限体系
│   └── audit.py       # 审计日志

第二阶段的 RAG 要建索引,长出 knowledge/

text
├── knowledge/
│   ├── index.py       # 第 11 篇:嵌入索引
│   └── runbooks/      # Runbook 文档

第四阶段的 MCP,长出 mcp/

text
├── mcp/
│   └── runbook_server.py  # 第 24 篇:MCP server

最后要对外提供服务,长出 app/

text
├── app/
│   └── main.py        # HTTP 入口

最终结构

全部决策落地后的目录:

text
ops-assistant/
├── app/               # HTTP 入口
├── agent/             # 对话与循环
├── tools/             # 外部系统访问
├── knowledge/         # 索引与搜索
├── harness/           # 权限、安全、审计
├── mcp/               # MCP 服务器
├── prompts/           # Prompt 模板
├── config.py          # 配置集中
├── data/
├── outputs/
└── pyproject.toml

每个目录对应一个明确职责,读者能很快知道打开哪个文件。目录深度控制在 3 层以内,超过 3 层说明拆分过度。

模块依赖方向

模块之间的依赖方向要清晰,不能乱:

AIOps 概念图:模块依赖方向

text
app/ → agent/ → tools/ / knowledge/ / mcp/

                  └── harness/(被任何模块调用,但不依赖业务逻辑)

app/ 只处理 HTTP,调 agent/agent/ 处理对话和循环,调 tools/knowledge/mcp/harness/ 可以被任何模块调用(权限检查到处都要),但不依赖具体业务逻辑。依赖方向单向,不循环。

app/agent/ 不要合并。有些项目把 HTTP 处理和 agent 逻辑放同一个文件,理由是"都是入口"。这会导致两个问题:本地 CLI 测试时要加载 HTTP 框架依赖;agent 逻辑升级时可能意外破坏 HTTP 接口。保持分离,通过清晰接口调用。

配置集中

第 2 篇建立的 settings 约定在这里固化。模型名、base_url、embedding 模型、agent 轮数、上下文预算、权限模式都放 config.py:

python
class Settings(BaseSettings):
    api_key: str
    base_url: str = ""
    primary_model: str = "gpt-5.5"   # 示例默认值,以账号可用模型为准
    fast_model: str = ""
    embedding_model: str = "text-embedding-3-large"
    max_agent_turns: int = 6
    max_input_tokens: int = 12000
    permission_mode: str = "default"
    mcp_refresh_interval: int = 300

工具函数里不要直接读 os.environ,切换模型或调预算时只改一处。primary_modelfast_model 的多模型策略在第 29 篇展开。

保留 CLI 入口

即使有 HTTP 服务,也要保留本地 CLI 入口。本地 CLI 用于快速复现问题——告警样本从文件读,跳过 HTTP 层,直接跑 agent 循环。排查 agent 行为问题时,CLI 比起 HTTP 服务快得多。

python
# cli.py
def main():
    alert = Path(sys.argv[1]).read_text(encoding="utf-8")
    engine = QueryEngine(client, load_settings())
    print(engine.run(alert))

if __name__ == "__main__":
    main()

测试同步建

tests/ 不是写完功能再补。每个新模块创建时,同步创建对应的测试文件,哪怕只是空框架。这强迫从"这个模块的输入输出是什么"的角度思考设计,而不是先写实现再回头想怎么测。第 5 篇讲的 Prompt 回归测试也在这里跑。

骨架之后

骨架搭好了,模块各就各位。下一篇把工具接进骨架——工具注册中心怎么动态组装工具池,工具排序和 prompt cache 的关系,返回格式怎么统一。