软件构造实验总结:从“跑通代码“到“架构重构“
起点一个跑不起来的项目实验一的第一步就卡住了——pip install -r requirements.txt 之后 python run.py 直接崩溃。FastAPI 报 ModuleNotFoundError: pydantic-settings装完又一个 ModuleNotFoundError: loguru再装再报……前前后后补了十几个缺失的依赖。更要命的是 bcrypt4.1 与 passlib 不兼容pillow 在 Python 3.13 下编译失败——版本地狱不过如此。这反而成了实验最好的开端真实世界的项目不会为你准备好一切。你读报错、翻源码、搜 PyPI、锁版本——工程师的第一课永远是让代码跑起来。项目本身挺有意思CogmAIt一个 FastAPI SQLAlchemy MySQL Neo4j Milvus 的 AI 中台。120 个文件插件化的 AI 提供商系统OpenAI / Anthropic / Google / Ollama 等 10 个支持文件解析、知识图谱查询、向量检索、MCP 工具调用。第一份见面礼冗余代码跑起来之后花了些时间扫了一遍代码发现了三处问题1. 永不调用的死函数——config.py 里的 get_openai_config() 和 update_openai_config()全局搜索零调用。OpenAI 配置早就迁到了 Pydantic Settings这是典型的重构后忘了打扫战场。2. 大段注释掉的旧代码——web_search.py 里 11 行被注释的 httpx 旧实现以及 agents.py 里 165 行被整块注释的 share_chat_with_agent 函数。注释掉的代码就像冰箱里过期的食物留着除了占地方不会有任何好处。3. 安全红线——生产环境的 print(self.api_key) 和 print(用户, payload) 还在往标准输出里打敏感信息。这大概率是调试时随手加的调试完忘了删。Poetry 迁移给项目一个正经的依赖管理原始项目用 requirements.txt没有版本锁定依赖项还不全。迁移到 Poetry 之后- 50 个依赖按功能分组Web / 数据库 / 认证 / AI / 向量 / 文件处理 / MCP- 开发依赖pytest、pytest-asyncio、pytest-cov进 dev group不会污染生产环境- poetry.lock 锁定 141 个包的精确版本任何人 poetry install 都能复现完全相同的环境这不是格式转换——是从能用就行到可复现构建的升级。架构师之路用 DI 拯救不可测试的代码实验一有个部分是把 config.py 从一个全局单例函数重构为可注入的 ConfigManager 类。原来的代码长这样_config None # 全局单例——测试间状态污染def load_config():global _configif _config is not None: # 缓存——测试 A 的 load 影响测试 Breturn _configif os.path.exists(config.json): # 硬编码路径——无法 Mock...if NEO4J_URI in os.environ: # 直接读环境变量——无法注入...这看着没什么问题直到你尝试写测试——每个测试用例都共享同一个 _config一个用例改了状态下一个用例继承的就是修改版。重构后class ConfigManager:def__init__(self, config_pathNone, envNone):self.config_path config_path or config.jsonself.env env if env is not None else os.environ # ← DI 注入点self._config None # ← 实例级隔离不是模块级单例每次测试传个临时文件和模拟的环境变量进去彼此完全隔离。最后写了 17 个测试用例覆盖率 79%。四个调用方完全不用改——模块级 load_config() 保持原签名内部委托给默认 ConfigManager。这就是重构反哺不是为了测试而重构架构而是架构本身就有问题测试只是让它暴露出来。面向对象的肌肉记忆新增 AI Provider实验二的任务一看起来简单新增一个 DeepSeek 的 AI 提供商让系统能调用 DeepSeek 的模型。真正有价值的是理解背后的设计。ModelProvider 是一个抽象基类定义了 9 个契约项5 个属性 4 个方法。而 ProviderManager 通过 pkgutil.iter_modules 自动扫描 providers/ 目录inspect.getmembers 自动发现所有 ModelProvider 的子类并注册。也就是说你只需要把新文件放进 providers/ 目录系统就自动识别了——不改 manager.py不改 routes不改任何其他代码。这就是开闭原则OCP对扩展开放对修改关闭。反思题问如果你新增 Provider 不得不改 agents.py 里的 if-else 分支说明违反了什么原则——答案 OCP。正确设计下agents.py 只和 ModelProvider 抽象接口对话不需要知道对面是 OpenAI 还是 DeepSeek。多态自动解决一切分发。实现本身也不复杂DeepSeek 兼容 OpenAI 协议用 aiohttp 直接调 HTTPS API。200 行搞定其中 text_completion 内部转为单条 chat 消息embedding 返回 not_supportedDeepSeek 不提供独立 embedding 端点。ADT 防具给数据穿上装甲同一个实验里还有另一个任务给聊天上下文做一个防弹衣。原本的代码中final_messages [] 是一个裸列表在系统提示词、文件上下文、网络搜索结果、MCP 输出、历史消息之间裸奔。任何一个中间处理步骤都可能意外修改数据而且完全没有任何检查——你往列表里塞一个字符串、一个 int、一个 None它都照单全收。SessionContext 的设计思路是1. AF 定义代表一次智能体对话的完整短期记忆按时间顺序保存消息2. RI 约束最多 100 条每条是 {role, content} 的 dictrole 只能是 system/user/assistantcontent 不能为空3. 防御性拷贝入的时候 copy.deepcopy() 再存出的时候再 copy.deepcopy() 再返回。外部怎么折腾都不影响内部状态4. 每次变更自检_check_rep() 在每次 add/insert 之后检查 RI。违规就抛 AssertionError不给错误状态存活的机会然后写了 20 个破坏性测试Happy Path × 7、RI Guard × 6、Defensive Copy × 4、Message Limit × 1、CheckRep × 2。非法 role拒绝。空 content拒绝。超过上限抛异常。改外部引用会不会影响内部不影响。20 passed, 0 failed。数据库没用 SQLite 文件跑测试而是用 MySQL 的 CREATE TEMPORARY TABLE 自动清理。这倒不是形式主义——之前用文件跑测试没问题但多个测试同时跑就容易锁表死锁。临时表每次自动销毁互相不干扰。决战上帝函数从 1650 行到 180 行实验三是整个实验里最重头的部分。正面硬刚chat_with_agent 函数重构前 1650 行承担了至少 8 项职责身份验证、文件处理、网络搜索、向量检索、图谱查询、MCP 调度、消息组装、模型推理。把这么多东西塞进一个函数不叫全栈叫技术债。读代码的时候发现这个函数已经在内部拆分了一些 helper但不够彻底——Web Search、MCP、Knowledge 仍是直接内联在函数中代码扫除也没做完残留 15 个未使用的 import165 行注释掉的旧实现。三步策略第一步服务下沉。先把最独立的两块拆出来——文件上下文处理DocumentContextService和网络搜索WebSearchService。文件处理跟其他逻辑没任何耦合网络搜索就一个 API 调用验证成本最低。跑通之后继续拆知识检索KnowledgeRetrievalService和图谱查询GraphRetrievalService。第二步策略模式统一。三个检索通道Web、Knowledge、Graph做同一件事给定 query、返回上下文。那就抽象出 BaseRetrievalStrategy一个 execute(query, db) - Dict 抽象方法。三个具体策略实现后主函数变成for strategy in active_strategies:result await strategy.execute(user_message, db)context.add_message(result[context_message])新加一个增强通道只需要新增一个策略类不改任何调度代码。又是 OCP。第三步流水线化。服务拆完了策略统一了但主函数里还有大量阶段性逻辑MCP 调度、消息组装、模型推理、历史持久化。这些阶段顺序固定、职责边界清晰——这就是流水线模式的教科书级用例。最终的流水线Audit - FileProcessing - Strategy - MCP - Inference - Persist6 个 Stage各自 CC ≤ 4共享一个 ChatContext dataclass。chat_with_agent 和 chat_with_agent_api 两个入口复用完全相同的流水线——唯一的区别是 ChatContext 里的认证字段。数据说话指标对比重构前agents.py 2547 行chat_with_agent 约 1650 行未使用 import 15 个死代码约 165 行服务类 0 个策略类 0 个Pipeline Stage 0 个测试 115 个重构后agents.py 821 行chat_with_agent 约 180 行未使用 import 0 个死代码 0服务类 4 个策略类 4 个Pipeline Stage 6 个测试 135 个CC 数字没变B(8) in → B(8) out这是因为 CC 只数 if/else/loop不数代码行数。真正的进步在于原来的 1650 行你改一行需要担心 1649 行的副作用现在的 180 行改一个 Stage 最多影响一个 Stage。写在最后三个实验加在一起写了 5000 行代码含测试、十几份文件、三个实验报告。有几点我不太想忘掉1. 能跑和能维护之间隔着一次重构。原始项目可以正常运行但代码的依赖方向纠缠、测试完全不可行。重构不是锦上添花是为了让下一次改动不变成灾难。2. 读代码比写代码难。读懂 1650 行的 chat_with_agent 花了两个晚上。看懂每个 if-else 分支、每个 try-except 块、每条数据流路径——然后才能在不改行为的前提下拆分它。重构的第一步永远是理解。3. 测试不是事后检查是设计工具。ConfigManager 的重构不是为了写测试——是因为测试试图钻进去却钻不进去暴露了架构本身的缺陷。测试是设计的气味探测器它轰然倒地的地方往往是架构最脆弱的地方。4. 好名字和好注释解决 80% 的问题。DocumentContextService、WebSearchService、StrategyStage、InferenceStage——这些名字本身就是文档。函数签名就是契约。代码不说谎前提是你得让它说清楚。