Files
2026-moving-helper/docs/design/step-2-llm-integration.md
T
tliu93 8b8bd9f38f
test / pytest (push) Successful in 1m34s
Add Alembic migration foundation
2026-06-01 16:02:43 +02:00

103 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 步骤 2 · LLM 接入 / Step 2 · LLM Integration
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §4;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
> **前置 / Prerequisite** [步骤 1](./step-1-alembic-foundation.md) 已合入(Alembic 已就位——**schema 变更一律通过新建迁移完成,并经迁移命令 `python -m app.migrate` / `db-migration` 步骤生效,非应用启动时**)。Step 1 merged; Alembic is in place — **schema changes go through a new migration, applied by the migration command, not at app startup**.
> **产出 / Output** 一个可独立合入的 PR。
---
## 目标 / Goal
提供一个配置页:能填写并测试 OpenAI 兼容的 `base_url`/`model`/`api_key`,配置落库到 `app_settings`;并提供一个可复用、可 mock 的 LLM 客户端。**未配置时整站行为不变。**
A settings page to enter & test the LLM config, persisted to `app_settings`, plus a reusable, mockable LLM client. **App behavior is unchanged when unconfigured.**
---
## 必要背景 / Essential Context
- 路由全部在 `app/main.py::create_app()`;模板在 `app/templates/`,基础模板 `base.html` 顶部有导航(现有「箱子」「搜索」两个链接)。
All routes live in `create_app()`; templates under `app/templates/`; nav lives in `base.html`.
- DB 会话依赖:`Depends(get_db)``app/db.py`)。models 在 `app/models.py``Base``app/db.py`
- **同步 handler 即可**FastAPI 把同步 `def` 路由丢线程池执行,阻塞式 `httpx` 调用可接受。
Sync handlers are fine — FastAPI runs them in a threadpool, so blocking `httpx` is acceptable.
- `httpx` 已在 `requirements.txt`**不要新增依赖**(不引入 `openai` SDK)。
`httpx` is already a dependency; **add no new deps**.
### 关键决策 / Key Decisions
- **配置存储用键值表**,不是定型列:`app_settings(key TEXT PRIMARY KEY, value TEXT)`。原因:后续配置项会变多,KV 加项=加一行、永不迁移;类型/校验在 Python 侧。
KV table, not typed columns — future settings = new rows, never a migration.
- **API Key 明文落库**(业主在其威胁模型下的明确选择),但**配置页绝不回显明文**:显示「已配置,留空=不修改」,提交留空则保留原值。
Plaintext key in DB (owner's explicit choice), but the **UI never echoes it** — show "configured, leave blank to keep".
- **优雅降级**`llm_enabled` 关或缺 `model`/`api_key` 时,`is_configured()` 为假;调用失败不抛 500,返回可识别的失败信号。
Graceful degradation throughout.
### 本轮使用的 key / Keys this round
| key | 含义 / Meaning | 默认 / Default |
| --- | --- | --- |
| `llm_enabled` | LLM 总开关 / master toggle | `false` |
| `llm_base_url` | OpenAI 兼容端点 / endpoint | `https://api.openai.com/v1` |
| `llm_model` | 模型名 / model name | (空 / empty |
| `llm_api_key` | API Key(明文 / plaintext | (空 / empty |
| `ai_search_enabled` | AI 搜索功能开关(步骤 3 用)/ AI-search toggle | `false` |
---
## 任务 / Tasks
- [ ] **新建 V2 迁移**(用 Alembic,遵循步骤 1 的工作流):创建 `app_settings(key TEXT PRIMARY KEY, value TEXT)`
New V2 Alembic migration creating `app_settings`.
- [ ] `app/models.py`:新增 `AppSetting` 模型(映射 `app_settings`)。
- [ ] 配置读写 helper(建议放 `app/settings_store.py``app/config.py` 旁):
- `get_app_settings(db) -> LLMConfig`dataclass`enabled`/`base_url`/`model`/`api_key`/`ai_search_enabled`,含默认值)。
- `save_app_settings(db, ...)`:写回 KV;Key 留空则不覆盖原值。
- [ ] 新增 `app/llm.py`(基于 `httpx`):
- [ ] `is_configured(cfg) -> bool`
- [ ] `test_connection(cfg) -> Result`(发最小请求验证 `base_url`/`model`/`api_key`)。
- [ ] `expand_query(cfg, query) -> list[str]`(查询词扩展;**步骤 3 会用**,本步先落地+单测)。
- [ ] 统一超时 + 错误处理;失败优雅降级。
- [ ] **(预留,不实现)** `analyze_image(...)`:仅留 TODO/签名占位 + 注释指向"未来图片分析轮次"。Reserved, not implemented.
- [ ] 把所有网络调用收敛到**单一函数边界**,便于测试整体 mock。
- [ ] 路由(`app/main.py`):
- [ ] `GET /settings`:渲染配置表单(Key 脱敏)。
- [ ] `POST /settings`:保存到 `app_settings`(303 重定向,沿用现有 POST 风格)。
- [ ] `POST /settings/test`:用当前/待保存配置测试连接,回显结果。
- [ ] 模板:`app/templates/settings/form.html`(沿用现有卡片/表单样式);`base.html` 导航加「设置」入口。
- [ ] 测试(LLM 全程 mock,CI 不联网):
- [ ] 保存/读取配置;**Key 脱敏**(响应 HTML 不含明文;提交留空不覆盖原 Key)。
- [ ] `POST /settings/test` 成功/失败两条分支(mock `test_connection` 或底层 httpx)。
- [ ] 未配置时 `is_configured()` 为假;配置页在 `llm_enabled=false` 下仍可正常打开保存。
---
## 涉及文件 / Files
`migrations/versions/**`(V2)、`app/models.py``app/llm.py`(新)、`app/settings_store.py`(新,或并入既有模块)、`app/main.py``app/templates/settings/form.html`(新)、`app/templates/base.html``tests/`
---
## 验收 / Acceptance
-`/settings` 填入配置 → 保存 → 重启应用后仍在(已落库)。Config persists across restarts.
- 「测试连接」对真实 OpenAI 端点可用(手动验证);自动化测试中走 mock。
- 配置页 HTML **不含明文 Key**;留空提交保留原值。
- `llm_enabled=false` 或缺 Key 时,全站行为与步骤 1 后一致(无回归)。
---
## 风险与缓解 / Risks & Mitigations
- **把网络调用散落各处 → 难 mock、CI 易联网。** 缓解:所有外呼集中在 `app/llm.py` 单一边界。
Scattered network calls → keep all egress in `app/llm.py`.
- **Key 不慎回显。** 缓解:模板永不输出 `api_key` 值,仅输出"是否已配置"。
Accidental key echo → template never prints the key value.
---
## 相关约定 / Conventions(详见 implementation-plan.md
- 不主动 push/commit,除非业主要求。
- 无新依赖(用 `httpx`)。CI 不联网(mock LLM)。
- 实现与设计若有偏差 → 回写设计文档 §4 与仓库简报 §15。