6.5 KiB
步骤 2 · LLM 接入 / Step 2 · LLM Integration
可独立执行 / Self-contained. 完整背景见设计文档
llm-integration-design.md§4;跨步骤约定见implementation-plan.md。 前置 / Prerequisite: 步骤 1 已合入(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 increate_app(); templates underapp/templates/; nav lives inbase.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 blockinghttpxis acceptable. httpx已在requirements.txt,不要新增依赖(不引入openaiSDK)。httpxis 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 creatingapp_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) -> booltest_connection(cfg) -> Result(发最小请求验证base_url/model/api_key)。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成功/失败两条分支(mocktest_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 inapp/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。