J.B-Lin commited on
Commit
56b0dc2
·
1 Parent(s): 8193cd7

docs: 部署成功报告 + 清理神秘文件 c

Browse files
Files changed (2) hide show
  1. c +0 -502
  2. docs/deploy_success_report.md +205 -0
c DELETED
@@ -1,502 +0,0 @@
1
- # 🌸 PregoPal 项目理解与技术架构(v4 - Loop 驱动 + 三文档操作)
2
-
3
- > 本文档已完成重构,每个小节末尾附有批注区,便于你边看边标注意见。
4
-
5
- ---
6
-
7
- ## 一、一句话定位
8
-
9
- **一个基于每日生命周期 Loop 驱动的孕期陪护 AI 助手**:每天首次启动时自动总结昨日饮食、对比国家营养标准(DRIs/膳食指南/体重标准),生成今日简报注入对话上下文;白天与家庭成员(通过声纹区分)自然交互,实时记录饮食、推荐食谱;次日循环往复。
10
-
11
- > **批注:** ________
12
-
13
- ---
14
-
15
- ## 二、三个营养文档的操作方法
16
-
17
- 你已经下载到 `data/nutrition/raw/` 的三个文件,各自承担不同角色:
18
-
19
- | 文档 | 文件 | 用途 | PregoPal 的操作方式 |
20
- |------|------|------|-------------------|
21
- | **① 体重增长标准** | `妊娠期妇女体重增长推荐值标准.md` | 体重监测基准 | **主动询问体重** — 每天 Loop 中检查是否需要询问,对照 BMI 分类给出评价 |
22
- | **② 膳食指南** | `中国孕期妇女膳食指南2022图片转md版.md` | 食谱推荐基准 | **作为推荐食谱的约束基础** — 根据孕期阶段用宝塔推荐量生成模板 |
23
- | **③ DRIs** | `备孕&孕早中晚三期和哺乳期的所有膳食营养素参考摄入量.md` | 营养分析基准 | **Agent 定期思考** — 每天自动对比 RNI,分析不足,写入次日预设 |
24
-
25
- > **批注:** ________
26
-
27
- ### 2.1 体重增长标准 → 主动询问体重
28
-
29
- **数据来源**:WS/T 801-2022 表1
30
-
31
- ```
32
- 每天早上 Loop 的 weight_check 插件:
33
- ├→ 检查 data/logs/ 中是否有近期的体重记录
34
- ├→ 若无记录或记录超过 7 天
35
- │ └→ 在今日简报中标记「需询问体重」
36
- │ (UI 弹出体重卡片,或语音询问:「今天称体重了吗?」)
37
- └→ 若有记录
38
- ├→ 计算当前孕周、孕前 BMI、当前总增长值
39
- ├→ 对照标准表:
40
-
41
- | 孕前 BMI 分类 | 总增长范围 | 每周增长(中+晚) |
42
- |----------------------|------------|----------------|
43
- | 低体重 BMI<18.5 | 11.0~16.0 | 0.46(0.37~0.56) |
44
- | 正常 18.5≤BMI<24.0 | 8.0~14.0 | 0.37(0.26~0.48) |
45
- | 超重 24.0≤BMI<28.0 | 7.0~11.0 | 0.30(0.22~0.37) |
46
- | 肥胖 BMI≥28.0 | 5.0~9.0 | 0.22(0.15~0.30) |
47
-
48
- ├→ 在范围内 → ✅ 鼓励「体重增长很好,继续保持!」
49
- └→ 超出范围 → ⚠️ 提醒「增长偏快/偏慢,建议调整饮食/咨询医生」
50
- ```
51
-
52
- > **批注:** ________
53
-
54
- ### 2.2 膳食指南 → 食谱推荐基础
55
-
56
- **数据来源**:中国孕期妇女平衡膳食宝塔(备孕/孕中期/孕晚期/哺乳期)
57
-
58
- ```
59
- 推荐食谱时,AI 的约束条件:
60
- ├→ 孕中期宝塔:
61
- │ 奶类 300-500g,鱼禽蛋肉类 150-200g,谷类 200-250g
62
- │ 蔬菜 400-500g,水果 200-300g,薯类 75g
63
- │ 大豆 20g,坚果 10g,油 25g,盐 5g
64
-
65
- ├→ 孕晚期宝塔:
66
- │ 奶类 300-500g,鱼禽蛋肉类 175-225g,谷类 225-275g
67
- │ 蔬菜 400-500g,水果 200-350g,薯类 75g
68
- │ 大豆 20g,坚果 10g,油 25g,盐 5g
69
-
70
- └→ 膳食指南六条核心建议(嵌入系统提示词):
71
- 1. 调整孕前体重至正常范围
72
- 2. 常吃含铁食物,选用碘盐,补充叶酸和维生素D
73
- 3. 孕吐严重者少量多餐
74
- 4. 孕中晚期增加奶、鱼、禽、蛋、瘦肉
75
- 5. 经常户外活动,禁烟酒
76
- 6. 愉快孕育新生命,积极准备母乳喂养
77
- ```
78
-
79
- **原有 MEAL_TEMPLATES 的改造**:不再硬编码模板字符串,而是将上述宝塔约束条作为结构化 JSON 传递给 AI(或作为本地 fallback 兜底),由 AI 根据宝塔约束 + 用户偏好生成个性化食谱。
80
-
81
- > **批注:** ________
82
-
83
- ### 2.3 DRIs → Agent 定期分析(自动运行 → 写入次日预设)
84
-
85
- **数据来源**:表15(孕早期)/ 表16(孕中期)/ 表17(孕晚期)/ 表18(哺乳期)
86
-
87
- ```
88
- 每天 Loop 的 dri_analysis 插件:
89
- ├→ 读取昨日饮食记录(data/logs/饮食日志_YYYY-MM-DD.md)
90
- ├→ 根据当前孕期阶段选择对应 DRIs 表
91
- ├→ 关键营养素逐项对比(重点标记 RNI 与摄入量差异):
92
-
93
- ┌─────────────────────────────────────────────┐
94
- │ 营养素 RNI(孕中期) 昨日摄入 差异 │
95
- ├─────────────────────────────────────────────┤
96
- │ 能量(MJ) 9.84(PAL II) 8.2 -17% │
97
- │ 蛋白质(g) 70 55 -21% │
98
- │ 钙(mg) 800 420 -48% │
99
- │ 铁(mg) 25 12 -52% ← 重点关注 │
100
- │ 叶酸(μg DFE) 600 350 -42% ← 重点关�� │
101
- │ 维生素D(μg) 10 2.1 -79% ← 重点关注 │
102
- └─────────────────────────────────────────────┘
103
-
104
- ├→ 生成分析结论(自然语言):
105
- │ 「昨天钙、铁、叶酸摄入偏低,建议今天多吃:
106
- │ ✓ 深绿色蔬菜(菠菜、西兰花)
107
- │ ✓ 瘦肉/动物肝脏
108
- │ ✓ 豆制品/奶制品」
109
-
110
- └→ 写入 data/presets/今日简报_YYYY-MM-DD.json
111
- ├ 今日重点关注营养素列表
112
- ├ 今日推荐食物类别与份量
113
- ├ 体重提醒(如需)
114
- └ 与孕妇及家人交谈的建议开场话题
115
- ```
116
-
117
- > **批注:** ________
118
-
119
- ---
120
-
121
- ## 三、核心架构:Loop 驱动(状态机 + 插件注册)
122
-
123
- ### 3.1 设计理念
124
-
125
- PregoPal 不是一个「用户点按钮 → 出结果」的被动应用,而是**有每日生命周期的主动伴侣**。
126
-
127
- 参考你提供的 `nanobot_core/agent/loop.py` 中的**状态机事件驱动模式**,PregoPal 的 Loop 提取两个核心设计:
128
-
129
- 1. **TurnState 状态机**(类比 `_process_message` 的 `_state_*` 状态链)— 管理每日生命周期的阶段流转
130
- 2. **可注册的扩展能力**(类比 ToolRegistry)— 每个阶段的具体逻辑由 Plugin 实现
131
-
132
- > **批注:** ________
133
-
134
- ### 3.2 每日生命周期
135
-
136
- ```
137
- ┌─────────────┐
138
- │ LAUNCH │ ← 首次启动检查
139
- │ (启动) │
140
- └──────┬──────┘
141
-
142
- ┌───────────┴───────────┐
143
- │ │
144
- 首次运行今日 已运行过今日
145
- │ │
146
- ▼ ▼
147
- ┌──────────────┐ ┌──────────────┐
148
- │ SUMMARIZE │ │ │
149
- │ (昨日总结) │ │ │
150
- │ ├ 读取饮食日志 │ │ │
151
- │ └ 检查体重 │ │ │
152
- └──────┬───────┘ │ │
153
- │ │ │
154
- ▼ │ │
155
- ┌──────────────┐ │ │
156
- │ ANALYZE │ │ │
157
- │ (营养分析) │ │ │
158
- │ └ 对比DRIs │ │ │
159
- └──────┬───────┘ │ │
160
- │ │ │
161
- ▼ │ │
162
- ┌──────────────┐ │ │
163
- │ BRIEF │ │ │
164
- │ (生成简报) │ │ │
165
- │ └ 写JSON │ │ │
166
- └──────┬───────┘ │ │
167
- │ │ │
168
- ▼ ▼ │
169
- ┌──────────────────────────────────┐ │
170
- │ INTERACT │◄─┘
171
- │ (白天交互模式) │
172
- │ ├ 声纹识别 → 区分说话人 │
173
- │ ├ 对话理解 → 记录饮食/推荐/闲聊 │
174
- │ └ 简报注入 → AI 知道今日关注点 │
175
- └──────────────┬───────────────────┘
176
-
177
- 用户结束今日
178
-
179
-
180
- ┌──────────────┐
181
- │ CONSOLIDATE │
182
- │ (晚间整理) │ ← 可选(关闭页面时触发)
183
- │ ├ 汇总今日 │
184
- │ └ 写明日预设 │
185
- └──────┬───────┘
186
-
187
-
188
- ┌──────────────┐
189
- │ DONE │
190
- └──────────────┘
191
- ```
192
-
193
- > **批注:** ________
194
-
195
- ### 3.3 状态机定义
196
-
197
- ```python
198
- # 状态枚举
199
- class LoopState(Enum):
200
- LAUNCH = "launch" # 启动检查:是否是今日首次运行
201
- SUMMARIZE = "summarize" # 昨日总结:��析昨日饮食/体重
202
- ANALYZE = "analyze" # 营养分析:对比 DRIs
203
- BRIEF = "brief" # 生成今日简报
204
- INTERACT = "interact" # 白天交互模式(等待用户操作)
205
- CONSOLIDATE = "consolidate" # 晚间整理
206
- DONE = "done"
207
-
208
- # 状态转移表(类比 nanobot_core 的 _TRANSITIONS)
209
- _TRANSITIONS = {
210
- (LoopState.LAUNCH, "first_run_today"): LoopState.SUMMARIZE,
211
- (LoopState.LAUNCH, "already_run"): LoopState.INTERACT,
212
- (LoopState.SUMMARIZE, "ok"): LoopState.ANALYZE,
213
- (LoopState.ANALYZE, "ok"): LoopState.BRIEF,
214
- (LoopState.BRIEF, "ok"): LoopState.INTERACT,
215
- (LoopState.INTERACT, "day_ended"): LoopState.CONSOLIDATE,
216
- (LoopState.CONSOLIDATE, "ok"): LoopState.DONE,
217
- }
218
- ```
219
-
220
- > **批注:** ________
221
-
222
- ### 3.4 状态处理器
223
-
224
- 每个状态对应一个 handler(类比 `_state_restore`, `_state_compact`, `_state_run` 等):
225
-
226
- | 状态 | Handler | 功能 |
227
- |------|---------|------|
228
- | `LAUNCH` | `_state_launch()` | 检查 `data/presets/` 中是否有今日简报 → 决定首次/已运行 |
229
- | `SUMMARIZE` | `_state_summarize()` | 遍历执行所有注册在 SUMMARIZE 阶段的 Plugin |
230
- | `ANALYZE` | `_state_analyze()` | 遍历执行所有注册在 ANALYZE 阶段的 Plugin |
231
- | `BRIEF` | `_state_brief()` | 遍历执行所有注册在 BRIEF 阶段的 Plugin |
232
- | `INTERACT` | `_state_interact()` | 等待用户交互(从 Gradio event queue 消费消息) |
233
- | `CONSOLIDATE` | `_state_consolidate()` | 遍历执行所有注册在 CONSOLIDATE 阶段的 Plugin |
234
- | `DONE` | `_state_done()` | 清理资源,标记今日完成 |
235
-
236
- > **批注:** ________
237
-
238
- ### 3.5 主循环入口
239
-
240
- ```python
241
- # loop.py 核心逻辑
242
- class PregoPalLoop:
243
- def __init__(self):
244
- self.plugins = PluginRegistry()
245
- self.state = LoopState.LAUNCH
246
- self._register_default_plugins()
247
-
248
- async def run(self) -> None:
249
- """主循环入口"""
250
- while self.state is not LoopState.DONE:
251
- handler_name = f"_state_{self.state.value}"
252
- handler = getattr(self, handler_name)
253
- event = await handler()
254
- next_state = _TRANSITIONS.get((self.state, event))
255
- if next_state is None:
256
- raise RuntimeError(f"No transition from {self.state} on {event}")
257
- self.state = next_state
258
-
259
- async def _state_summarize(self) -> str:
260
- """SUMMARIZE 阶段:执行所有相关插件"""
261
- for plugin in self.plugins.get_plugins(LoopStage.SUMMARIZE):
262
- await plugin.run(self.context)
263
- return "ok"
264
- ```
265
-
266
- > **批注:** ________
267
-
268
- ---
269
-
270
- ## 四、插件系统设计(可扩展性)
271
-
272
- ### 4.1 插件基类
273
-
274
- ```python
275
- # plugins/base.py
276
- from abc import ABC, abstractmethod
277
- from dataclasses import dataclass, field
278
- from enum import Enum
279
-
280
- class LoopStage(Enum):
281
- """插件可注册的阶段"""
282
- SUMMARIZE = "summarize"
283
- ANALYZE = "analyze"
284
- BRIEF = "brief"
285
- CONSOLIDATE = "consolidate"
286
-
287
- @dataclass
288
- class LoopContext:
289
- """插件上下文:共享数据容器"""
290
- briefing: dict = field(default_factory=dict) # 今日简报数据
291
- weight_data: dict = field(default_factory=dict) # 体重记录
292
- diet_records: list = field(default_factory=list) # 饮食记录
293
- analysis_results: dict = field(default_factory=dict) # 分析结果
294
- errors: list = field(default_factory=list) # 错误收集
295
-
296
- @dataclass
297
- class PluginResult:
298
- """插件执行结果"""
299
- success: bool
300
- data: dict = field(default_factory=dict)
301
- message: str = ""
302
-
303
- class LoopPlugin(ABC):
304
- """Loop 扩展插件基类 — 所有插件继承此类"""
305
-
306
- @abstractmethod
307
- def stage(self) -> LoopStage:
308
- """此插件注册到哪个阶段"""
309
-
310
- @abstractmethod
311
- def name(self) -> str:
312
- """插件唯一名称"""
313
-
314
- @abstractmethod
315
- async def run(self, ctx: LoopContext) -> PluginResult:
316
- """执行插件逻辑"""
317
- ```
318
-
319
- > **批注:** ________
320
-
321
- ### 4.2 内置插件清单
322
-
323
- | 插件 | 类名 | 注册阶段 | 功能 | 数据源 |
324
- |------|------|---------|------|--------|
325
- | 昨日饮食总结 | `DietSummaryPlugin` | SUMMARIZE | 读取昨日饮食日志,汇总三餐 | `data/logs/*.md` |
326
- | 体重检查 | `WeightCheckPlugin` | SUMMARIZE | 检查体重记录,对比 WS/T 801 | `data/logs/*.md` + 用户输入 |
327
- | DRIs 分析 | `DRIAnalysisPlugin` | ANALYZE | 对比营养素参考摄入量 | DRIs 表 + 昨日饮食 |
328
- | 简报生成 | `BriefingGeneratorPlugin` | BRIEF | 汇总分析为结构化 JSON | `LoopContext` |
329
- | 预设写入 | `PresetWriterPlugin` | CONSOLIDATE | 生成明日预设文件 | `data/presets/` |
330
-
331
- > **批注:** ________
332
-
333
- ### 4.3 扩展方式
334
-
335
- ```python
336
- # 后续新增功能(如血糖监测)只需两步:
337
-
338
- # 1. 写一个新插件
339
- class BloodSugarPlugin(LoopPlugin):
340
- def stage(self) -> LoopStage: return LoopStage.SUMMARIZE
341
- def name(self) -> str: return "blood_sugar"
342
- async def run(self, ctx: LoopContext) -> PluginResult:
343
- # 读取血糖记录 → 分析趋势 → 写入 ctx
344
- ...
345
-
346
- # 2. 注册到 loop
347
- loop.plugins.register(BloodSugarPlugin())
348
-
349
- # 不需要修改 loop.py 的任何状态机代码
350
- ```
351
-
352
- > **批注:** ________
353
-
354
- ---
355
-
356
- ## 五、整体文件结构
357
-
358
- ```
359
- PregoPal/
360
- ├── app.py # 主入口:启动 Gradio + Loop
361
- ├── config.py # 全局配置(路径/枚举/系统提示词模板)
362
- ├── utils.py # 工具函数
363
- ├── loop.py ← 🆕 新增 # 核心循环引擎(状态机驱动)
364
-
365
- ├── plugins/ ← 🆕 新增 # Loop 插件
366
- │ ├── __init__.py
367
- │ ├── base.py # LoopPlugin 基类 + PluginRegistry
368
- │ ├── diet_summary.py # 昨日饮食总结
369
- │ ├── weight_check.py # 体重检查(WS/T 801)
370
- │ ├── dri_analysis.py # DRIs 营养素对比分析
371
- │ ├── briefing_generator.py # 今日简报生成器
372
- │ └── preset_writer.py # 预设文件写入
373
-
374
- ├── core/ # AI 核心层(等待 MiniCPM-o 部署)
375
- │ ├── __init__.py
376
- │ ├── model_loader.py # 模型加载器(空接口)
377
- │ ├── voice_processor.py # 语音处理器(空接口)
378
- │ ├── vision_processor.py # 视觉处理器(空接口)
379
- │ └── conversation_manager.py # 对话管理器(空接口)
380
-
381
- ├── modules/ # 业务逻辑层
382
- │ ├── __init__.py
383
- │ ├── voiceprint.py # 声纹识别
384
- │ ├── meal_recommender.py # 菜品推荐
385
- │ ├── diet_logger.py # 饮食记录 + Markdown 生成
386
- │ └── nutrition_analyzer.py # 营养分析 + 可视化
387
-
388
- ├── ui/ # 表现层
389
- │ ├── __init__.py
390
- │ └── app_builder.py # Gradio 界面 + 事件绑定
391
-
392
- ├── data/ # 数据存储
393
- │ ├── nutrition/
394
- │ │ └── raw/ # ← 你已下载的三个 MD 文件
395
- │ │ ├── 妊娠期妇女体重增长推荐值标准.md
396
- │ │ ├── 中国孕期妇女膳食指南2022图片转md版.md
397
- │ │ └── 备孕&孕早中晚三期和哺乳期的所有膳食营养素参考摄入量.md
398
- │ ├── presets/ ← 🆕 新增 # 每日简报 / 预设文件
399
- │ ├── voices/ # 声纹数据
400
- │ ├── logs/ # Markdown 饮食日志
401
- │ └── reports/ # 导出报告
402
-
403
- ├── docs/
404
- │ ├── 项目理解_技术架构.md # ← 本文档
405
- │ └── 开发日志.md
406
-
407
- ├── requirements.txt # 依赖清单
408
- └── README.md
409
- ```
410
-
411
- > **批注:** ________
412
-
413
- ---
414
-
415
- ## 六、数据流全景
416
-
417
- ```
418
- data/nutrition/raw/
419
- ├── 体重标准.md ─────────────┐
420
- ├── 膳食指南.md ─────────────┤
421
- └── DRIs.md ─────────────────┤
422
-
423
- ┌───────────────────────▼──────────────────────┐
424
- │ data/presets/ │
425
- │ (缓存编译后的结构化数据) │
426
- │ 体重标准.json 膳食指南.json DRIs.json │
427
- └──────────────┬────────────────────────────────┘
428
-
429
-
430
- ┌──────────────────────────────────────────────────────┐
431
- │ loop.py │
432
- │ ┌────────────────────────────────────────────────┐ │
433
- │ │ 状态机驱动 (LAUNCH→SUMMARIZE→ANALYZE→BRIEF) │ │
434
- │ │ ↓ 依次调用 Plugin │ │
435
- │ │ diet_summary → weight_check → dri_analysis │ │
436
- │ │ ↓ │ │
437
- │ │ briefing_generator │ │
438
- │ │ ↓ │ │
439
- │ │ data/presets/今日简报.json │ │
440
- │ └────────────────────────────────────────────────┘ │
441
- └──────────────────────┬───────────────────────────────┘
442
-
443
-
444
- ┌──────────────────────────────────────────────────────┐
445
- │ ui/app_builder.py (Gradio 界面) │
446
- │ ┌────────────────────────────────────────────────┐ │
447
- │ │ Tab1 声纹识别 │ Tab2 菜品推荐 │ Tab3 饮食记录 │ │
448
- │ │ Tab4 营养报告 │ Tab5 关于 │ │
449
- │ │ │ │
450
- │ │ 今日简报注入对话上下文 │ │
451
- │ │ AI 知道:今天该关注什么营养素 │ │
452
- │ │ 该问谁体重?该聊什么话题? │ │
453
- │ └────────────────────────────────────────────────┘ │
454
- └──────────────────────┬───────────────────────────────┘
455
-
456
-
457
- modules/diet_logger.py (记录新数据)
458
- modules/meal_recommender.py (膳食指南约束)
459
- modules/nutrition_analyzer.py (DRIs 对比)
460
-
461
-
462
- 次日 loop.py 再次启动 → 循环
463
- ```
464
-
465
- > **批注:** ________
466
-
467
- ---
468
-
469
- ## 七、技术栈一览
470
-
471
- | 层级 | 技术 | 用途 |
472
- |------|------|------|
473
- | 核心模型 | **MiniCPM-o 4.5**(统一多模态模型) | 全双工推理:视觉+语音+文本+语音输出 |
474
- | 前端框架 | **Gradio 6.x** | Web 界面,实时对话交互 |
475
- | 声纹识别 | Whisper-medium encoder embedding(优先)| 说话人身份识别 |
476
- | 频谱方案 | NumPy + SoundFile(备选 fallback) | 声纹识别兜底 |
477
- | 数据存储 | JSON + Markdown | 饮食记录 + 报告存档 + 每日简报 |
478
- | 营养标准 | 三个 MD 文件(已下载) | 体重标准/膳食指南/DRIs |
479
- | 循环引擎 | 自研状态机 + 插件注册 | 每日生命周期管理 |
480
- | 部署平台 | HuggingFace Spaces(目前)+ Modal(后续) | 云端部署 |
481
-
482
- > **批注:** ________
483
-
484
- ---
485
-
486
- ## 八、待讨论问题清单
487
-
488
- > 📝 下表汇总了本文档中需要你决策的问题。请在 `____` 处标注你的意见(如需更多讨论,在对应小节批注区补充)。
489
-
490
- | # | 问题 | 你的选择 / 意见 |
491
- |---|------|----------------|
492
- | 1 | **Loop 启动时机**:Gradio 启动时先跑完 Loop 总结阶段再开服务(A)/ 用户打开页面时才触发(B) | ________ |
493
- | 2 | **简报承载形式**:JSON 文件落盘作为中介(A)/ 仅内存变量不落盘(B) | ________ |
494
- | 3 | **Loop 复杂度边界**:只需管好每日生命周期 3~5 个阶段,约 200~300 行(同意)/ 需要更复杂(请说明) | ________ |
495
- | 4 | **插件优先级**:DietSummary P0 / WeightCheck P0 / DRIAnalysis P0 / BriefingGenerator P0 / PresetWriter P1(需要调整?) | ________ |
496
- | 5 | **体重数据来源**:用户手动输入(A)/ 语音对话 AI 询问记录(B)/ 蓝牙同步未来扩展(C) | ________ |
497
- | 6 | **core/ 层处理**:现阶段 Loop 直接调用 modules/ 规则引擎,core/ 保持空接口等 MiniCPM-o(同意?) | ________ |
498
- | 7 | **还有什么我没考虑到的问题?** | ________ |
499
-
500
- ---
501
-
502
- *文档生成时间:2026-06-08 | v4 - 引入 Loop 驱动架构 + 三文档操作方案*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/deploy_success_report.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚀 PregoPal × MiniCPM-o-4_5 Modal 部署成功报告
2
+
3
+ > **状态**: ✅ 已成功部署到 Modal (T4 GPU)
4
+ > **日期**: 2026-06-09 ~ 2026-06-10
5
+ > **API URL**: `https://andrew-jiabin--prego-pal-minicpm-serve.modal.run`
6
+ > **GPU**: T4 (16GB VRAM, $0.50/hr) ← 从 A100 ($1.50/hr) 降级
7
+
8
+ ---
9
+
10
+ ## 1. 最终架构
11
+
12
+ ```
13
+ ┌─────────────┐ HTTP ┌──────────────────────────────────┐
14
+ │ prego-pal │ ──────────→ │ Modal T4 (asgi_app) │
15
+ │ (Gradio UI) │ │ ├── FastAPI (端点包装) │
16
+ └─────────────┘ │ └── llama-cpp-python (CUDA) │
17
+ │ ↕ │
18
+ │ Modal Volume (GGUF 模型持久化) │
19
+ └──────────────────────────────────┘
20
+ ```
21
+
22
+ ## 2. 关键决策与经验
23
+
24
+ ### 2.1 预编译 wheel vs 源码编译
25
+
26
+ | 方案 | 构建时间 | 稳定性 | 最终选择 |
27
+ |------|---------|--------|---------|
28
+ | **预编译 llama-cpp-python wheel** | ~1min ✅ | 稳定 ✅ | **✅ 采用** |
29
+ | 源码编译 llama.cpp + llama-server | ~8min | 易踩坑 | ❌ 放弃 |
30
+
31
+ **核心经验**: 只要最终目标是 Python 调用,`llama-cpp-python` 的预编译 CUDA wheel (`extra-index-url=https://ggml-org.github.io/llama-cpp-python/whl/cu121`) 就足够了,完全不需要走源码编译路线。
32
+
33
+ ### 2.2 为什么预编译 wheel 能跑
34
+
35
+ ```
36
+ deploy.py (FastAPI) → llama-cpp-python (Llama class)
37
+
38
+ MiniCPM-o-4_5 GGUF (Q4_K_M)
39
+
40
+ CUDA (T4 GPU, n_gpu_layers=-1)
41
+ ```
42
+
43
+ - llama-cpp-python 内部封装了完整的 llama.cpp C++ 后端
44
+ - 通过 `mmproj=` 参数挂载视觉投影层,支持多模态
45
+ - GGUF 格式量化模型 (Q4_K_M) 在 T4 上推理速度 ~15-25 tok/s
46
+
47
+ ### 2.3 T4 降级可行
48
+
49
+ | 资源 | 用量 | T4 容量 |
50
+ |------|------|---------|
51
+ | 主模型 (Q4_K_M) | ~4.7 GB | 16 GB |
52
+ | Vision mmproj | ~0.5 GB | |
53
+ | KV Cache (8192 ctx) | ~1 GB | |
54
+ | **总计** | **~6.2 GB** | **余量 >60%** |
55
+
56
+ ### 2.4 镜像构建关键
57
+
58
+ ```python
59
+ _image = (
60
+ Image.debian_slim(python_version="3.11")
61
+ .pip_install("fastapi", "uvicorn[standard]", "httpx", "numpy", "Pillow")
62
+ .pip_install(
63
+ "llama-cpp-python", # ← 预编译 wheel
64
+ extra_index_url="https://ggml-org.github.io/llama-cpp-python/whl/cu121",
65
+ )
66
+ .run_commands(
67
+ "python -c 'import llama_cpp; print(\"llama-cpp-python OK\")'",
68
+ )
69
+ )
70
+ ```
71
+
72
+ **构建时间**: ~45秒(纯 pip install,无编译)
73
+
74
+ ---
75
+
76
+ ## 3. 最终 deploy.py 核心逻辑
77
+
78
+ ```python
79
+ # deploy.py 中的关键参数
80
+ @app.function(
81
+ image=_image,
82
+ volumes={MODEL_DIR: model_volume}, # 挂载 Volume
83
+ gpu="T4", # ← T4 降级
84
+ timeout=1200,
85
+ scaledown_window=300, # 5min 空闲缩容
86
+ )
87
+ @modal.concurrent(max_inputs=10) # 10 并发
88
+ @asgi_app()
89
+ def serve():
90
+ # 模型加载
91
+ llm = Llama(
92
+ model_path="/models/MiniCPM-o-4_5-gguf/MiniCPM-o-4_5-Q4_K_M.gguf",
93
+ mmproj="/models/MiniCPM-o-4_5-gguf/vision/MiniCPM-o-4_5-vision-F16.gguf",
94
+ n_gpu_layers=-1, # 全部层到 GPU
95
+ n_ctx=8192, # 8K 上下文
96
+ verbose=False,
97
+ )
98
+
99
+ # 暴露端点: POST /v1/chat/completions (OpenAI 兼容)
100
+ # POST /v1/vision (多模态)
101
+ # GET /health (健康检查)
102
+ ```
103
+
104
+ 完整代码见 `modal_deploy/deploy.py`。
105
+
106
+ ---
107
+
108
+ ## 4. 踩坑与修复
109
+
110
+ ### 坑①:`find_mmproj_files()` 搜索到 9 个 GGUF
111
+
112
+ **症状**: 旧代码递归搜索所有 `.gguf` 文件并全部作为 `--mmproj` 传入,llama-server 启动失败
113
+
114
+ **修复**: 显式指定 vision mmproj 路径,不自动搜索
115
+
116
+ ```python
117
+ # ❌ 错误做法
118
+ mmproj_files = find_mmproj_files(MODEL_SUBDIR) # 找到 9 个 GGUF!
119
+ llama_server_cmd += [f"--mmproj", mmproj_file for mmproj_file in mmproj_files]
120
+
121
+ # ✅ 正确做法
122
+ VISION_MMPROJ = "vision/MiniCPM-o-4_5-vision-F16.gguf"
123
+ if os.path.isfile(os.path.join(MODEL_SUBDIR, VISION_MMPROJ)):
124
+ kwargs["mmproj"] = os.path.join(MODEL_SUBDIR, VISION_MMPROJ)
125
+ ```
126
+
127
+ ### 坑②:源码编译 llama.cpp 构建超 8 分钟
128
+
129
+ **症状**: `modal deploy` 时构建镜像耗时 458 秒,且 cmake 步骤容易超时
130
+
131
+ **修复**: 改用预编译 llama-cpp-python wheel,构建时间降至 ~45 秒
132
+
133
+ ### 坑③:Volume 路径不匹配
134
+
135
+ **症状**: 模型路径 `/MiniCPM-o-4_5-gguf/...` 需要子目录前缀
136
+
137
+ **修复**:
138
+ ```python
139
+ MODEL_SUBDIR = f"{MODEL_DIR}/MiniCPM-o-4_5-gguf"
140
+ ```
141
+
142
+ ### 坑④:500 Internal Server Error
143
+
144
+ **症状**: 模型加载成功但 API 返回 500
145
+
146
+ **修复**: 增加 try/except + traceback 日志��定位到参数拼写错误
147
+
148
+ ---
149
+
150
+ ## 5. 测试结果
151
+
152
+ | 测试项 | 结果 |
153
+ |--------|------|
154
+ | 中文对话 | ✅ "你好,我是 MiniCPM-o" |
155
+ | 英文对话 | ✅ "Paris" |
156
+ | 健康检查 GET /health | ✅ 200 |
157
+ | 模型列表 GET /v1/models | ✅ 200 |
158
+ | Streaming SSE | ✅ 逐 token 输出 |
159
+ | 冷启动时间 | ~40-60s |
160
+ | 推理速度 (T4) | ~15-25 tok/s |
161
+
162
+ ---
163
+
164
+ ## 6. 命令行速查
165
+
166
+ ```bash
167
+ # 部署
168
+ modal deploy modal_deploy.deploy
169
+
170
+ # 开发模式
171
+ modal serve modal_deploy.deploy
172
+
173
+ # 测试推理
174
+ modal run modal_deploy.deploy::test_inference
175
+
176
+ # 查看日志
177
+ modal app logs prego-pal-minicpm
178
+
179
+ # 上传模型
180
+ modal volume put minicpm-o-4_5-models ./models/MiniCPM-o-4_5-gguf /
181
+
182
+ # 查看 Volume
183
+ modal volume ls minicpm-o-4_5-models /MiniCPM-o-4_5-gguf
184
+ ```
185
+
186
+ ## 7. 成本对比
187
+
188
+ | GPU | 单价/小时 | 每次请求(2s) | 每月 1000 次 |
189
+ |-----|----------|-------------|-------------|
190
+ | **T4** (当前) | **$0.50** | **$0.00028** | **$0.28** |
191
+ | A100 (旧) | $1.50 | $0.00083 | $0.83 |
192
+ | **节省** | **67%** | **67%** | **67%** |
193
+
194
+ ---
195
+
196
+ ## 8. 文件清单
197
+
198
+ | 文件 | 说明 |
199
+ |------|------|
200
+ | `modal_deploy/deploy.py` | **主部署文件** — FastAPI + llama-cpp-python |
201
+ | `modal_deploy/client.py` | Python API 客户端 (OpenAI 兼容) |
202
+ | `modal_deploy/README.md` | 部署文档 |
203
+ | `core/model_loader.py` | 项目集成层 (默认走远端 API) |
204
+ | `_test_inference.py` | 测试脚本 |
205
+ | `docs/部署经验_Modal_MiniCPM-o.md` | 旧版部署经验(含源码编译方案) |