HIT2026软件构造实验二的问题以及解决
实验二手记:在 CogmAIt 里把 OOP 与 ADT 真正「接上线」
这学期软件构造的实验二,标题听起来很「架构」——ADT、OOP、可插拔、防具式上下文。说实话,我一开始更担心的是:环境能不能跑起来、Swagger 点下去会不会又是一片红。真正做完之后,我发现实验想练的不是「背概念」,而是在真实仓库里找主路径、在脏数据里活下来、在能跑的前提下做最小改造。下面按时间线,把我踩过的坑和怎么爬出来的写清楚,也给以后的自己留个档。
1. 我先搞明白:到底改哪里才算「生效」
实验文档一开始就提醒:仓库里可能有旧代码、路径要确认。我做的第一件事不是写 Provider,而是打开app/main.py和app/api/v1/api.py,确认路由前缀是/api而不是我脑子里默认的/api/v1。后面所有截图、curl、Swagger,我都按这个前缀来,避免「文档写得对、我测得不对」这种低级错位。
和实验最相关的几条主链路,我最后锁定在:
agents.py:聊天主流程models.py:模型 CRUD 和测试providers/base.py+manager.py:抽象契约与扫描加载utils/model.py:统一推理入口
这一步看起来「没产出」,但后面每次报错,我至少知道该去哪个文件对线。
2. 任务一:接三家厂商之前,环境先把我教育了一遍
2.1 MySQL、Poetry、依赖
我本地用 MySQL。最早.env里还残留过 SQLite 的写法,和课程要求不一致,后来改成DB_*指向本机实例,才算和「模型、智能体落库」这条链路对齐。
Poetry 安装依赖时,根目录不是一个标准 Python 包,装全局会报「找不到 package」。我在pyproject.toml里加了package-mode = false,这才顺利用poetry install把环境拉齐。
登录注册那块缺bcrypt,报错信息很直白,补上依赖后 Swagger 注册/登录才稳定。
2.2 Swagger 登录:为什么一直是Bearer undefined
这不是模型问题,是 OAuth2 配置问题。OAuth2PasswordBearer的tokenUrl必须指向真正返回access_token的接口。我们项目里登录返回字段叫token一类,和 Swagger 默认想象的不完全一致;把tokenUrl改到/api/auth/token这类正确路径后,Authorize 里才能拿到真正的 Bearer。
另外还有/auth/me之类接口:ORM 字段和响应模型字段对不上会直接 500。那种错误很像「后端炸了」,其实是 Pydantic 校验失败。我后来学会先看响应体里的detail,再对照 schema。
2.3 三个 Provider:接口不是「能跑就行」,而是要「长得像一家人」
我按文档继承了ModelProvider,实现了chat_completion、embedding、test_connection等契约方法。实现上我走了 OpenAI 兼容 SDK(AsyncOpenAI)这条最省心的路:DeepSeek、智谱、Moonshot 都按兼容层去调。
这里我踩过一个很蠢但很常见的坑:把 Swagger 模板里的"string"、"null"当真值写进数据库。尤其是base_url:写成带引号的"null"时,底层会当成 URL 去解析,直接UnsupportedProtocol;status写成字面量string时,推理入口会提示「模型状态不是活动状态: string」——字面意思就是:你存进去的状态真的叫string,当然不 active。
厂商控制台里 402 Insufficient Balance 我也遇到过。那一刻我差点怀疑 Provider 写错了,后来才意识到:这是钱的问题,不是代码问题。换 key 如果还是同一个零余额账号,照样 402;充值或换「有余额账号」的 key 后,同一份代码立刻正常。
2.4 对话接口:stream: false为什么「200 但 code 500」
这个坑很「后端特色」:HTTP 200 只是网关层,业务包装里code仍然是 500。我们后来定位到:非流式路径函数没有 return,中间件包一层就变成「服务器内部错误」。修完之后又要面对async for拿到 dict:推理层在错误场景会返回{"error": ...},但聊天生成器把它当异步迭代器去async for,Python 直接抛__aiter__。
再往后是「成功但正文为空、tokens 为 0」:本质是 chunk 形态多样,只认某一种delta.content会漏。把解析写宽一点之后,我才在 Swagger 里截到那张最爽的图:code=200,message.content有字,tokens_used也大于 0。
任务一最后的反思题我也真的想过:如果每加一个厂商都要改agents.py的 if-else,那确实违反 开闭原则(OCP)——扩展应该主要靠新增子类与扫描加载,而不是改核心路由。
3. 任务二:我不想做一个「漂亮但挂不上线」的 SessionContext
3.1 为什么从final_messages下手
agents.py太长,我先用全局搜索找final_messages看它怎么「长出来」:文件上下文、系统提示、检索结果等前缀先拼,最后才把chat_request.messages那段用户可见多轮对话 append 进去。文档要求至少替换一段装配逻辑,我就选这段:风险最小、又最贴近实验描述的 chat messages。
3.2 AF / RI 不是写给老师看的,是写给我自己用的
我先写SessionContext的 Docstring:AF 写清楚「抽象上它代表什么」,RI 写清楚「内部允许什么、不允许什么」。后面_check_rep基本就是把这些话翻译成 assert。写细一点的好处很明显:实现阶段我会不断问自己——这句规则到底要不要 enforce?
add_message先deepcopy再校验,最后只保留role和content两个键;get_messages再deepcopy整表返回。测试里我故意对返回值append,确认内部条数不变——那一刻我才真的理解「防具」不是形容词,是能挡住什么。
3.3 接入与两条入口
主聊天路径改完后,我发现还有 API Key 那条聊天入口也在拼消息。只改一条,未来一定不一致,所以我把那条也统一成同一模式。
4. 进阶:我挑了两条「能写完、也能讲清楚」的加分项
我不想一口吃成胖子去「重构整个记忆子系统」,那更像另一个大作业。我选的是:
不可变
KnowledgeChunk:在process_text_chunks向量化前,把List[str]提升成Tuple[KnowledgeChunk, ...],批内用kc.text/kc.chunk_index去对齐 embedding 与入库字段。它解决的是「块级文本与序号」这一小段流水线里的表示泄露问题。VectorStoreProvider+ 两种内存实现:抽象接口 +ListMemoryVectorStore+DictMemoryVectorStore。我没有幻想短期替换 Milvus,而是在同一批数据写入 Milvus 之前,并行镜像到两套内存后端——足够在报告里讲清楚多态与解耦的意图,又不把线上存储结构赌进去。
这两项都有独立pytest,本地不依赖 Milvus 也能验。
5. 写报告时我遇到的「最后一个坑」:LaTeX 图乱飘
我一开始用figure[htbp],图总跑到奇怪的位置。后来用float包的[H]强制就地;第一次还忘了\usepackage{float},编译器直接报Unknown float option 'H'。另外\linewidth超过 1.0 会溢出,这些属于「写论文/写报告的肌肉记忆」,我也顺便长了一点。
6. 我最大的收获(比分数更重要)
实验二让我把几件事打通了:
- 扩展:尽量靠抽象接口与扫描加载,而不是靠改核心路由分支。
- ADT:先写清 AF/RI,再写代码,比先堆代码再补注释靠谱得多。
- 工程现实:
402、status=string、OAuth、非流式 return,这些看起来像「玄学」的东西,多数都有非常具体的解释路径。 - 最小改造:先切最小口、能回归、能解释,再考虑「架构师幻想时间」。
如果读者你也正在做类似实验:建议你把自己的「第一次成功对话」截图留下来——那不是终点,但它会是你整个实验二里最像奖励关卡的一帧。
加油!
