Appearance
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/ 模块:

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 层说明拆分过度。
模块依赖方向
模块之间的依赖方向要清晰,不能乱:

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_model 和 fast_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 的关系,返回格式怎么统一。