Files
2026-moving-helper/docs/design/step-2-llm-integration.md
T

103 lines
6.5 KiB
Markdown
Raw Normal View History

# 步骤 2 · LLM 接入 / Step 2 · LLM Integration
> **可独立执行 / Self-contained.** 完整背景见设计文档 [`llm-integration-design.md`](./llm-integration-design.md) §4;跨步骤约定见 [`implementation-plan.md`](./implementation-plan.md)。
2026-06-01 16:02:43 +02:00
> **前置 / 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`)。
2026-06-01 21:28:29 +02:00
- [ ] `expand_query(cfg, query) -> ExpansionResult`(查询词扩展;**步骤 3 会校准提示词与输出契约**;`terms` 为扩展词列表,`error` 用于区分超时/网络/HTTP 等真实调用失败)。
- [ ] 统一超时 + 错误处理;失败优雅降级。
- [ ] **(预留,不实现)** `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。