- memory/contracts.md:立 C6-ext(load_prompt/SPECS/SCHEMA_CATALOG/
REVIEW_RESERVED_NAMES/SpecResolver + 打包契约)+ 变更日志一笔
- memory/decisions.md:方案A 决策(运行时值金标准/尾换行方案B/守卫前移/
schema 留 Python/同一实例)
- memory/gotchas.md:spec.name 连字符命名 / 改稿须重生成金标准 /
.gitattributes 完整嵌套路径 / wheel artifacts 带 .md / structlog 补声明 /
SpecResolver 零 DB + 精确匹配
- PROGRESS.md:已封板「Prompt 管理重构 + 全量重写」波次
- ARCHITECTURE.md §5.1:补 prompt 外置 + SPECS/SCHEMA_CATALOG/SpecResolver,
内置 agent 计数 8→21
- packages/{llm_gateway,core}/pyproject.toml:补声明 structlog>=24.1
(原直接 import 未声明,靠 apps/api 传递;裸装 import ww_agents 会缺)
64 KiB
网文创作工作流 · 工程架构文档(Architecture)
把 PRODUCT_SPEC.md(产品规格)下沉到可实现的技术架构:组件划分、接口契约、完整 DDL、LLM 网关内部设计、编排引擎、API 清单、横切关注点、部署与测试。 UX/UI 见 UX_SPEC.md。本文按 PRODUCT_SPEC 章节顺序逐节展开,每节末标注回溯锚点
← PRODUCT_SPEC §x。
0. 文档说明
| 项 | 内容 |
|---|---|
| 文档类型 | 软件架构文档(SAD),面向工程实现 |
| 目标读者 | 后端/前端工程师、技术负责人、SRE |
| 上游 | PRODUCT_SPEC.md(产品规格)、UX_SPEC.md(界面规格) |
| 范围 | 技术架构与设计;不含业务代码与脚手架(属 M1 实现阶段) |
| 状态 | 设计定稿待评审;评审通过后据此搭 M1 骨架 |
| 关键约束 | 提供商中立(不绑定单一 LLM 厂商);记忆即真相源;AI 不黑箱(产出经验收 gate) |
与上游的关系:PRODUCT_SPEC 回答"做什么/为什么",本文回答"怎么实现"。本文不重复产品论证,只在每节顶部用一句话点出对应的产品意图,随后给技术方案。如展开中发现规格需调整,集中列于 §12 后 · 待回写规格的发现,不擅自改上游。
1. 架构总览 ← PRODUCT_SPEC §1 / §1.5 / §2
1.1 质量属性(NFR)与架构驱动
产品的四大痛点 + 工程哲学,转译为可度量的架构驱动力:
| 质量属性 | 目标 | 架构手段 |
|---|---|---|
| 一致性(首要) | 几十万字不崩设定/不忘伏笔 | 记忆即真相源 + 每章事实摘要 + 写前检索 + 四审 gate |
| 可扩展(提供商) | 任意接入/切换 LLM 厂商 | LLM 网关 + 适配器 + 能力档位抽象 |
| 可扩展(能力) | 新增 Agent/题材模板零侵入 | 声明式 Skill + registry + 表权限契约 |
| 成本可控 | 长记忆注入不烧钱 | 前缀缓存抽象 + 确定性按需注入(显式+近况,无向量) + 档位分流 |
| 可用性/韧性 | 单厂商故障不致瘫 | 故障回退链 + 重试/熔断 + 能力降级 |
| 安全/隔离 | 密钥与作品数据隔离 | 密钥托管 + 用户 Skill 沙箱(原型单用户;多租户行级隔离为后续,见 §9.1) |
| 可掌控(产品约束) | AI 改动经作者裁决 | 四审只读 + 验收事务写回 |
| 可观测 | 每次 LLM 调用可追踪可计费 | 结构化日志 + LLM trace + 按 provider+model 记账 |
1.2 架构风格与关键决策(ADR 摘要)
| # | 决策 | 选择 | 理由 / 取舍 |
|---|---|---|---|
| ADR-1 | 总体形态 | 前后端分离:后端为模块化单体(Modular Monolith)起步 | 前端 Next、后端 FastAPI 两个可部署单元;后端内部编排器/网关/记忆为清晰模块、预留拆服务接缝,避免过早微服务化。 |
| ADR-2 | 真相源 | 数据库为唯一真相源,Agent 经库交换 | 对齐 PRODUCT_SPEC §5.4;Agent 间零直连,降耦合、可独立替换。 |
| ADR-3 | LLM 接入 | 自建网关 + 适配器,直连各家 API | 对齐 §3.4;提供商中立、确定性编排;不用厂商 CLI/托管运行时(锁定单厂商)。 |
| ADR-4 | Agent 形态 | 声明式 Skill = 单一职责 Agent;用 LangGraph 做确定性编排 | 对齐 §5;非自治 agent loop,编排为固定图(节点/并行/验收 HITL 中断/checkpoint),可测、可回放。 |
| ADR-5 | 写章模型 | 章 = 纯函数 f(大纲, 选择的状态, 文风),状态变更集中在验收 | 可缓存、可重放;副作用隔离到验收事务。 |
| ADR-6 | 检索 | Postgres 单库 + 确定性记忆选择(无向量) | 原型设定集不大,"本章有谁"大纲已点名,确定性选择(显式+主角+近况)足够且可调试;向量检索降为 P2,规模化再在同一接口后接入。 |
| ADR-7 | 结构化输出 | 网关统一封装,原生优先、JSON+校验兜底 | 各家能力参差(§3.3 能力协商),上层无感。 |
| ADR-8 | 前后端 | 前端 Next.js(TS)+ 后端 FastAPI(Python)分离 | 前端 SSR/流式友好;后端 Python 是 LLM 编排/工具的强生态;契约经 OpenAPI→TS 代码生成。 |
| ADR-9 | 长任务 | FastAPI BackgroundTasks + jobs 表(原型不上专门队列) |
学文风/全书扫描走后台任务 + 状态表;写章/审稿走 SSE 即时反馈。规模化再上 arq/Celery。 |
| ADR-10 | 编排实现 | LangGraph(Python) | 写章→四审→验收天然是跨请求的暂停-恢复(HITL);LangGraph 的并行/中断/checkpoint 正合,且 Python 版成熟。 |
1.3 系统上下文图(C4 L1)
┌───────────────┐
│ 网文作者 │ 浏览器
└──────┬────────┘
│ HTTPS
┌────────▼─────────────────────────────┐
│ 网文创作工作流(本系统) │
│ 立项·设定·大纲·写章·四审·验收·伏笔 │
└───┬──────────────────────────────────┘
│ 各家 API(经网关)
┌────────▼────────┐
│ LLM 提供商 │
│ Claude/DeepSeek │
│ Kimi/GPT/GLM/… │
└─────────────────┘
外部依赖:①LLM 提供商(多家,经网关);②对象存储(可选,存文风样本原文/导出稿)。
注:原型不依赖 Embedding 服务(砍向量检索,改确定性记忆选择,§3.4);向量检索为 P2,届时再引入嵌入服务。
1.4 容器图(C4 L2)
┌──── 前端 Next.js(TS) ────┐ ┌──────────── 后端 FastAPI(Python) ────────────┐
│ 作品库 立项 工作台 审稿 │ fetch │ │
│ 设定库 伏笔看板 设置页 │ ─/SSE─▶ │ API 层 ─▶ 编排器(LangGraph) ─▶ LLM 网关 │
└──────────────────────────┘ │ │ │ │ ┌────────┐│
OpenAPI→TS 客户端类型 │ │ ▼ ├─▶│OpenAI兼容││ DeepSeek…
│ │ 记忆服务(选择/注入/写回) ├─▶│Anthropic ││
│ ▼ │ └─▶│Gemini ││
│ BackgroundTasks 数据访问层(Repo) 密钥管理 │
└────┬───────────────┬───────────────┬─────────┘
│(jobs) │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ jobs 表(同库)│ │ Postgres │ │ 密钥存储 │
│ │ │ (真相源,无向量)│ │ (KMS/加密列) │
└─────────────┘ └─────────────┘ └─────────────┘
容器职责
- 前端(Next.js/TS):UX_SPEC 的页面与交互;流式渲染、冲突标注、乐观更新;经 OpenAPI 生成的 TS 客户端调后端。
- 后端(FastAPI/Python):API 层(入参校验、SSE)、编排器(LangGraph)、网关、记忆服务同进程。
- 编排器(LangGraph):确定性图串联 Agent(写章→四审→验收 HITL),并行四审、验收中断/恢复。
- 记忆服务:写前确定性选择(显式+主角+近况,无向量)、prompt 组装(缓存断点)、验收写回(事务)。
- LLM 网关:档位路由、适配器、能力协商/降级、回退、缓存、记账(§4 详)。图中 OpenAI 兼容适配器一套覆盖 DeepSeek/Kimi/Qwen/GLM/OpenAI,Anthropic/Gemini 各一适配器(§4.2)。
- 数据访问层:Repository 模式封装所有表读写(原型单用户,多租户行级隔离为后续)。
- BackgroundTasks +
jobs表:学文风、全书扫描等长任务(原型不上专门队列)。 - 密钥管理:各提供商 API Key 加密托管。
2. 技术栈与工程结构 ← PRODUCT_SPEC §3.1
2.1 选型与理由
| 层 | 选型 | 理由 |
|---|---|---|
| 前端语言/框架 | TypeScript + Next.js App Router + React | SSR/流式/编辑器友好;纯 UI,调后端 API |
| 后端语言/框架 | Python + FastAPI(async) | LLM 编排/工具的强生态;async + SSE + Pydantic 校验天然契合 |
| 样式 | Tailwind + CSS 变量(设计 token) | 纸感主题 token 化(§8.4);快速一致 |
| 编排 | LangGraph(Python) | 写章→四审→验收的并行 + HITL 中断 + checkpoint(ADR-10) |
| 数据库 | PostgreSQL(无向量) | 关系一库;事务保证验收写回原子性;向量检索为 P2 再引入 |
| ORM/迁移 | SQLAlchemy 2.0(async)+ Alembic | 类型化 + 迁移管理;贴近 SQL/索引控制 |
| 长任务 | FastAPI BackgroundTasks + jobs 表 |
原型免专门队列;规模化再上 arq/Celery |
| LLM SDK | anthropic + openai(baseURL 覆盖 DeepSeek/Kimi/Qwen/GLM) + google-genai |
网关内适配器按需选用 |
| 校验/结构化输出 | Pydantic + instructor |
入参 + 结构化输出 schema 单一来源 |
| 前后端契约 | FastAPI OpenAPI → 生成 TS 客户端(openapi-typescript/orval) | 找回"共享类型",跨语言不漂移 |
| 鉴权 | 原型单用户 stub(多租户 + Auth 为后续) | 先做原型;§9.1 |
| 观测 | OpenTelemetry + 结构化日志(structlog) | LLM 调用 trace、成本指标 |
说明:本架构约束模块边界与契约;上表为原型锁定栈,向量检索/专门队列/多租户均为后续可选(标注于各节)。
2.2 代码组织(monorepo 模块边界)
writer-work-flow/
├── apps/
│ ├── web/ # 前端 Next.js(TS):纯 UI,调后端 API
│ │ ├── app/ # App Router 页面(对齐 UX_SPEC §6)
│ │ │ ├── (dashboard)/ # 作品库
│ │ │ ├── projects/[id]/ # 工作台: write/outline/codex/foreshadow/review/style/rules
│ │ │ └── settings/ # 模型与提供商设置(UX §6.10)
│ │ ├── components/ # UI 组件(对齐 UX §7)
│ │ └── lib/api/ # OpenAPI 生成的 TS 客户端
│ └── api/ # 后端 FastAPI(Python)
│ ├── routers/ # 端点(对齐 §7)
│ └── main.py # 应用入口 + OpenAPI
├── packages/ # Python 包(后端共享)
│ ├── core/ # 领域核心(无 IO)
│ │ ├── domain/ # 实体/状态机(伏笔状态机/一致性规则)
│ │ ├── orchestrator/ # 编排器(LangGraph 图)
│ │ └── memory/ # 确定性选择/注入/prompt 组装
│ ├── llm_gateway/ # LLM 网关(适配器/路由/降级/回退/缓存/记账)
│ │ └── adapters/ # openai_compatible / anthropic / gemini
│ ├── agents/ # 8 个内置 Agent 的声明(prompt+schema+reads/writes)
│ ├── skills/ # Skill registry + loader + 权限沙箱
│ ├── db/ # SQLAlchemy 模型 / Alembic 迁移 / Repository
│ ├── shared/ # Pydantic schema / 错误信封(后端; 经 OpenAPI 暴露给前端)
│ └── config/ # 提供商配置/档位映射/env 解析
└── PRODUCT_SPEC.md / UX_SPEC.md / ARCHITECTURE.md / DEV_PLAN.md
模块依赖方向(高→低,禁止反向):
apps/api(routers) → core / agents / skills → llm_gateway / memory → db → shared。
core 不依赖 db 的具体实现,只依赖 Repository 接口(依赖倒置,便于测试 mock)。
前后端契约:apps/api 的 FastAPI 自动产出 OpenAPI → 生成 apps/web/lib/api 的 TS 类型,跨语言不漂移。
2.3 运行时拓扑
- 后端进程(FastAPI):处理 HTTP/SSE,承载 API 层 + 编排器(LangGraph) + 网关 + 记忆服务(同进程,低延迟);长任务用 BackgroundTasks 在同进程后台跑,状态落
jobs表(原型无独立 Worker;常驻服务部署即可,避免 serverless 杀长函数)。 - 前端进程(Next.js):SSR + 静态资源;调后端 API。
- Postgres:单主库起步;读多可加只读副本(看板/列表查询走副本)。
- 横向扩展:原型单实例即可;后端无状态,规模化可多实例 + 抽独立 Worker(arq/Celery)。
3. 数据架构 ← PRODUCT_SPEC §3.2
产品意图:记忆即真相源,每章写前检索注入、验收后增量更新;不可变追加保留历史。
3.1 完整 DDL(11 张创作表 + 运营表)
类型以 PostgreSQL 表述;
jsonb用于结构化但 schema 演进频繁的字段。所有业务表带project_id外键(原型单用户,多租户行级隔离为后续)。原型无向量列——记忆选择走确定性逻辑(§3.4);向量检索为 P2,届时再加embedding vector(N)列与 HNSW 索引。 下面先列 11 张创作数据表(对齐 PRODUCT_SPEC §3.2;其中timeline/decisions为 P2,MVP 可不建),再列 运营/系统表(用户/凭据/用量/技能/jobs)。
-- 作品(根表,承载总纲/卖点/结构)
CREATE TABLE projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL REFERENCES users(id),
title text NOT NULL,
genre text,
logline text, -- 一句话故事
premise text, -- 总纲
theme text, -- 立意
selling_points jsonb DEFAULT '[]', -- 卖点数组
structure text, -- 故事结构选型(三幕/故事圈/雪花)
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
-- 人物(设定库 / 角色生成器产出)
CREATE TABLE characters (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name text NOT NULL,
role text, -- 定位: 主角/CP/对手/导师/工具人
traits jsonb, -- 性格(核心-表层-阴影/大五)
appearance text,
motive text,
backstory text, -- 背景故事
arc jsonb, -- 人物弧光: 起点→转变→终点
speech_tics jsonb, -- 口癖/语言风格
tags jsonb DEFAULT '[]', -- 人设标签/萌点
relations jsonb DEFAULT '[]', -- 关系网边: [{target, type}]
first_chapter int,
latest_state text, -- 最新状态(随验收更新)
created_at timestamptz NOT NULL DEFAULT now()
);
-- 世界观实体(势力/地理/力量体系/物品)
CREATE TABLE world_entities (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
type text NOT NULL, -- faction/place/power_system/item/rule
name text NOT NULL,
rules jsonb, -- 硬规则(供一致性校验引用)
first_chapter int,
latest_state text,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 大纲(分卷分章 + 场景)
CREATE TABLE outline (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
volume int NOT NULL,
chapter_no int NOT NULL,
beats jsonb, -- 节拍/场景清单
foreshadow_windows jsonb DEFAULT '[]', -- 本章关联的伏笔回收窗口(可多条)
UNIQUE (project_id, chapter_no)
);
-- 章节事实摘要(append-only, 防矛盾的关键)
CREATE TABLE chapter_digests (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
chapter_no int NOT NULL,
facts jsonb NOT NULL, -- 结构化事实: 谁做了什么/状态变化/新增设定
created_at timestamptz NOT NULL DEFAULT now()
);
-- 伏笔账本
CREATE TABLE foreshadow (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
code text NOT NULL, -- F-012
title text NOT NULL,
status text NOT NULL DEFAULT 'OPEN', -- OPEN/PARTIAL/CLOSED/OVERDUE
planted_at int, -- 埋设章号
content text,
expected_close_from int,
expected_close_to int,
importance text, -- 主线/支线
links jsonb DEFAULT '[]', -- 关联人物/势力
progress jsonb DEFAULT '[]', -- [{chapter, note, status}] append
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (project_id, code)
);
-- 文风指纹
CREATE TABLE style_fingerprint (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
dimensions_json jsonb NOT NULL, -- 16 维: {dim: value}
evidence_json jsonb NOT NULL, -- 每维原文证据
version int NOT NULL DEFAULT 1, -- 增量更新版本
created_at timestamptz NOT NULL DEFAULT now()
);
-- 四级规则
CREATE TABLE rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid REFERENCES projects(id) ON DELETE CASCADE, -- global 级可为 NULL
level text NOT NULL, -- global/genre/style/project
content text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 章节正文(带版本, 支持改稿回溯)
CREATE TABLE chapters (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
volume int NOT NULL,
chapter_no int NOT NULL,
content text,
status text NOT NULL DEFAULT 'draft', -- draft/accepted
version int NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (project_id, chapter_no, version)
);
-- 审稿留痕(四审报告 + 裁决; 支撑 UX 审稿历史; append 每次审稿)
CREATE TABLE chapter_reviews (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
chapter_no int NOT NULL,
chapter_version int, -- 审的是哪个草稿版本
conflicts jsonb DEFAULT '[]', -- continuity 冲突清单
foreshadow_sug jsonb DEFAULT '[]', -- 新埋/回收建议
style jsonb, -- 漂移段 + 评分
pace jsonb, -- 注水/钩子/节拍图
health_score int, -- 0-100
decisions jsonb, -- 作者裁决(采纳/忽略/回炉)留痕
created_at timestamptz NOT NULL DEFAULT now()
);
-- 时间线(P2; MVP 先由 chapter_digests 推导)
CREATE TABLE timeline (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
event text NOT NULL,
chapter_no int,
story_time text, -- 故事内时间(可模糊)
created_at timestamptz NOT NULL DEFAULT now()
);
-- 设定讨论决议留痕(P2)
CREATE TABLE decisions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
topic text NOT NULL,
decision text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
运营/系统表(非创作数据)
-- 用户(多租户主体; owner_id 外键指向此)
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
email text UNIQUE NOT NULL,
display_name text,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 提供商凭据(各家 API Key, 加密存储; 按用户/作品)
CREATE TABLE provider_credentials (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id uuid REFERENCES projects(id) ON DELETE CASCADE, -- NULL=用户级默认
provider text NOT NULL, -- anthropic/deepseek/kimi/openai/...
api_key_enc bytea NOT NULL, -- 加密密文(KMS/加密列), 永不回显
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (owner_id, project_id, provider)
);
-- 档位路由配置(全局默认在 config; 作品级覆盖落库)
CREATE TABLE tier_routing (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid REFERENCES projects(id) ON DELETE CASCADE, -- NULL=用户/全局
tier text NOT NULL, -- writer/analyst/light
provider text NOT NULL,
model text NOT NULL,
fallback jsonb DEFAULT '[]', -- [{provider,model}] 回退链
UNIQUE (project_id, tier)
);
-- 用量记账(按 provider+model 计费, 支持多币种)
CREATE TABLE usage_ledger (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_id uuid NOT NULL REFERENCES users(id),
project_id uuid REFERENCES projects(id),
provider text NOT NULL,
model text NOT NULL,
input_tokens int NOT NULL,
output_tokens int NOT NULL,
cache_read int DEFAULT 0,
cost_minor bigint NOT NULL, -- 最小货币单位
currency text NOT NULL, -- USD/CNY...(混币种)
created_at timestamptz NOT NULL DEFAULT now()
);
-- 技能 registry(内置/自定义/社区; 对齐 PRODUCT_SPEC §5.5)
CREATE TABLE skills (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scope text NOT NULL, -- builtin/custom/community
owner_id uuid REFERENCES users(id), -- custom 时归属
name text NOT NULL,
description text,
tier text NOT NULL, -- 档位 或 provider:model
system_prompt text NOT NULL,
input_schema jsonb,
output_schema jsonb,
reads jsonb DEFAULT '[]', -- 声明式表读权限
writes jsonb DEFAULT '[]', -- 声明式表写权限
genre text,
examples jsonb DEFAULT '[]',
created_at timestamptz NOT NULL DEFAULT now()
);
-- 异步长任务状态(BackgroundTasks + 轮询; §7.4)
CREATE TABLE jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid REFERENCES projects(id) ON DELETE CASCADE,
kind text NOT NULL, -- style_learn / full_scan / batch_chars
status text NOT NULL DEFAULT 'queued', -- queued/running/done/failed
progress int DEFAULT 0, -- 0-100
result jsonb,
error text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
3.2 索引设计
-- 租户 + 高频过滤
CREATE INDEX idx_characters_proj ON characters(project_id);
CREATE INDEX idx_world_proj ON world_entities(project_id);
CREATE INDEX idx_digests_proj_ch ON chapter_digests(project_id, chapter_no);
CREATE INDEX idx_chapters_proj_ch ON chapters(project_id, chapter_no);
CREATE INDEX idx_foreshadow_proj_status ON foreshadow(project_id, status); -- 看板/到期扫描
-- 可选: 实体按名匹配(确定性检索的 FTS 兜底, §3.4)
CREATE INDEX idx_char_name_trgm ON characters USING gin (name gin_trgm_ops);
CREATE INDEX idx_world_name_trgm ON world_entities USING gin (name gin_trgm_ops);
-- (P2 引入向量时再加 embedding 列 + HNSW 索引)
3.3 不可变与版本化策略
| 数据 | 策略 | 说明 |
|---|---|---|
章节摘要 chapter_digests |
append-only | 每章一行,从不改;历史可回溯"第 X 章发生了什么" |
伏笔进展 foreshadow.progress |
数组 append | 状态字段可变,但进展只追加 |
章节正文 chapters |
多版本行 | 改稿写新 version 行,旧版保留;(project_id, chapter_no, version) 唯一 |
| 文风指纹 | 版本号递增 | 增量补样本生成新 version |
| 设定(人物/世界观) | 就地更新 + latest_state | 冲突不静默覆盖,标 [CONFLICT] 待裁决;重大变更可在 decisions 留痕 |
不可变原则呼应用户全局编码规范(不就地覆盖、保留历史)。验收写回为单事务(§5.5),保证摘要/伏笔/状态一致落库或全回滚。
3.4 确定性记忆选择(无向量)
原型用确定性选择取代向量检索——"本章有谁"大纲已点名,设定集也不大,无需语义相似度去猜,且完全可调试。
- 选择来源(并集去重):
- 显式引用——本章大纲
beats点名的人物/世界观实体(最精准)。 - 主角常驻——
role为主角/核心的角色永远纳入。 - 近况实体——近 N 章
chapter_digests.facts里出现过的实体(N 可配,默认 3–5)。 - (可选)按名匹配——正文/大纲提到的实体名,用 Postgres
pg_trgm/全文匹配兜底(§3.2 trgm 索引)。 - 命中本章
outline.foreshadow_windows的伏笔。
- 显式引用——本章大纲
- 渲染为卡片:每个选中实体行渲染成一张"角色/设定卡"文本块(喂给 AI 的"MD"由行生成,非手管文件)。
- 拼装:选中卡 + latest_state → "易变状态块",置于 prompt 缓存断点之后(§4.6)。
- 目的:只注入相关设定(控成本)+ 覆盖关键实体(抗上下文衰减),对齐 PRODUCT_SPEC §3.5。
- 已知局限(覆盖盲区):若本章涉及一个既未在 beats 点名、又久未出场(不在近 N 章)的实体(如回收 50 章前的伏笔、重提冷门角色),确定性选择会漏注入→ AI 可能写出与旧设定冲突的内容——这正是产品要防的失败。缓解:①命中
outline.foreshadow_windows的伏笔关联实体强制纳入;②对正文/大纲做按名pg_trgm匹配兜底;③允许作者在工作台手动 pin 实体进本章注入。这是砍向量换简单的代价,向量检索(P2)能从语义上补这个盲区。 - P2 升级位:设定集膨胀或盲区频发时,在
select_relevant_entities()同一接口后接入向量检索(加embedding列 + HNSW + 嵌入服务),上层无感。
3.5 数据访问层(Repository)
- 每张表一个 Repository(
CharacterRepo/ForeshadowRepo…),封装 SQL,统一强制project_id过滤(数据隔离;多租户化时此层再加owner_id校验)。 core依赖 Repository 接口而非实现(依赖倒置),测试时注入内存实现。- 迁移:Alembic 版本化迁移,CI 校验 schema 与 SQLAlchemy 模型一致。
4. LLM 网关架构 ← PRODUCT_SPEC §3.3 / §3.4 / §3.5
产品意图:不绑定单一厂商;Agent 只声明能力档位,网关映射到具体 provider+model,并屏蔽各家能力差异。
4.1 统一内部接口契约
网关对上层(编排器/Agent)暴露单一接口,屏蔽各家差异:
class Block(BaseModel):
text: str
cache: bool = False # 稳定块可标缓存断点
class LlmRequest(BaseModel):
tier: Literal["writer", "analyst", "light"] # 档位(或显式 provider:model 锁定)
system: list[Block] = [] # 稳定块在前
input: str | list[Block] # 易变内容(断点之后)
stream: bool = False
output_schema: type[BaseModel] | None = None # 需结构化输出时(Pydantic 模型/instructor)
thinking: bool = False # 是否启用思考/推理(有则启用)
max_tokens: int | None = None
scope: "Scope" # {project_id; user_id 原型可固定}
class Usage(BaseModel):
provider: str; model: str
input_tokens: int; output_tokens: int
cache_read_tokens: int = 0
cost_minor: int; currency: str # 混币种: USD/CNY…
class LlmResponse(BaseModel):
text: str
parsed: BaseModel | None = None # output_schema 命中时的结构化结果
usage: Usage
served_by: "ServedBy" # {provider; model; fell_back}
- 流式:
stream=True返回AsyncIterator[Delta],归一各家 SSE 事件为统一Delta。 - 上层永远不碰具体厂商字段——换厂商对 Agent 透明。
4.2 适配器设计
LlmRequest(统一)
│
┌────────▼─────────┐
│ 网关核心 │ 路由→能力协商→(缓存)→调用→(回退)→记账
└────────┬─────────┘
│ 归一化请求
┌─────────────┼──────────────┐
▼ ▼ ▼
OpenAI兼容适配器 Anthropic适配器 Gemini适配器
(DeepSeek/Kimi/ (Messages API (generateContent)
Qwen/GLM/OpenAI) 形态)
- 适配器职责:把
LlmRequest翻译成目标家 API 请求;把响应/流/usage 翻译回统一形态;声明能力矩阵(见 4.4)。 - OpenAI 兼容适配器一套覆盖多家:DeepSeek/Kimi/Qwen/GLM/OpenAI 仅 baseURL + model + key 不同。
- Anthropic、Gemini 各一适配器(请求形态不同)。
- 新增厂商 = 新增一个适配器 + 在
config注册,零侵入上层。
4.3 档位路由与三级配置解析
解析优先级(高→低):单 Agent 锁定 > 作品级覆盖 > 全局默认
tier ──▶ resolve(scope) ──▶ { provider, model, fallback[] }
- 配置来源:
config(全局默认)+ 作品设置(DB)+ Skill 内tier/provider:model(§5.6)。 - 解析结果含主模型 + 回退链(UX §6.10 的"主/回退")。
- 切换厂商只改配置,不改 Agent 代码(对齐 ADR-3)。
4.4 能力协商与降级
各家能力参差,网关维护能力矩阵并优雅降级:
| 能力 | 支持时 | 不支持时(降级) |
|---|---|---|
| 结构化输出(JSON Schema) | 走原生 | JSON 提示 + Pydantic/instructor 校验 + 重试(上限 2-3 次,超限标"该审未完成"不死循环) |
| 前缀缓存 | 标记缓存断点 | 跳过缓存(不影响正确性,仅成本) |
| 思考/推理 | 启用 | 普通生成 |
| 工具调用 | 原生 | 本系统四审/生成均用结构化输出实现,不强依赖工具调用 |
- 能力矩阵由适配器声明(
capabilities()),网关据此决定调用形态。 - 降级对上层透明,仅在
usage/日志标注实际行为。 - 轻量档可靠性风险:最便宜模型(Qwen-turbo/GLM-flash 类)JSON 结构化输出不稳,易触发重试。故 pace/style 这类轻量档审稿要用上面的重试上限兜底;若某 provider 在轻量档反复失败,运营可在档位路由把它换成更稳的模型(仍属轻量档)。
4.5 故障回退与重试
调用主模型
├─ 成功 ────────────────────────▶ 返回(标 served_by.fell_back=false)
├─ 限流(429)/超时/5xx ─▶ 指数退避重试(SDK/自实现, 上限 R 次)
│ └─ 仍失败 ─▶ 切回退链下一个 provider
└─ 内容策略拒绝 ─────────▶ 按策略可切回退或上抛(让作者知情)
回退链耗尽 ─▶ 抛 LlmUnavailable(上层降级: 提示作者换档位/稍后重试; 不丢正文)
- 中途失败:流式写章已吐部分 token 后回退耗尽 → 已生成部分由自动保存留在
chapters(draft),LangGraph write 节点报错使图停在 write 前的 checkpoint;前端提示"生成中断,可重试或基于已存部分续写",不丢稿。 - 熔断:某 provider 连续失败超阈值,短时熔断、直接走回退,避免雪崩。
- 缓存隔离:回退到不同 provider 时缓存失效(缓存按
provider+model维度,§4.6)。 - 重试只对幂等的生成/校验;写库副作用在编排层、不在网关。
4.6 缓存抽象(稳定前缀)
- 原则跨厂商通用:
system中的稳定内核(世界观硬规则 + 文风指纹 + 少量真正定型的角色)置于缓存断点前;易变(活跃角色的latest_state+ 本章大纲)置于断点后。 - 务实预期:真正稳定的多是世界观硬规则 + 文风指纹;角色
latest_state每次出场验收都变、新角色随时加入,所以"稳定内核"会随连载缩水、失效更频繁。"省约 90%" 仅在稳定内核占比高时成立,不是通用承诺;缓存收益应按命中率实测评估(§9.3)。 - 机制按厂商封装:Claude 显式
cache_control;OpenAI/DeepSeek 自动前缀缓存;不支持者跳过。 - 失效面:缓存键含
provider+model;上游稳定块任一字节变更即失效(故组装时排序序列化、不混入时间戳/UUID)。 - 命中指标回传
usage.cache_read_tokens,供观测(§9.3)。
4.7 凭据管理与隔离
- 各提供商 API Key 加密存储(KMS 或 DB 加密列),前端永不回显明文(UX §6.10)。
- 作用域:平台级默认 Key(可选)+ 用户/作品级自带 Key;解析时按 scope 选取。
测试连接:发一个最小探测请求验证 Key 有效 + 拉取能力矩阵。
4.8 用量记账
- 每次调用落
usage_ledger表(provider/model/in/out/cache_read/cost),按owner_id+project_id+provider+model+day聚合。 - 成本换算表随 provider 配置维护(单价随厂商而异,§3.3)。
- 供 UX §6.10 月度概览 + 按档位/作品下钻。
5. Agent 编排引擎 ← PRODUCT_SPEC §3.6 / §5
产品意图:单一职责 Agent + 确定性编排;Agent 经记忆库交换、不直连;四审只读、产出经验收 gate。
5.1 Agent / Skill 声明式抽象
内置 Agent 与用户 Skill 同构——都是一份声明,由编排器加载后经网关执行:
class AgentSpec(BaseModel):
name: str # worldbuilder / writer / continuity ...
tier: Tier # 能力档位(网关解析 provider+model)
system_prompt: str # 角色与约束(值由 load_prompt(name) import 期注入, 见下)
input_schema: type[BaseModel] # 入参契约
output_schema: type[BaseModel] | None # 结构化产出契约(网关保证; writer 为 None=纯文本)
reads: list[TableName] # 声明式表读权限
writes: list[TableName] # 声明式表写权限(经验收才生效)
genre: str | None = None # 题材适用(Skill 用)
- 内置 21 Agent 即
scope=builtin的 AgentSpec;用户 Skill 为custom/community(§5.6)。 reads/writes是契约:运行时强制,越权拒绝(安全见 §5.6 / §9.2)。- Prompt 外置(方案A,2026-06):
system_prompt散文不再内联于 Python 常量,而外置为packages/agents/ww_agents/prompts/<spec.name>.md,由load_prompt(name)(prompt_loader.py)在 import 期确定性加载(去 BOM/LF 归一/NFC/rstrip尾 LF + 内存缓存 + fail-fastPromptNotFoundError,无任何运行期插值)。system_prompt仍是字节稳定的整块str,进system且cache=True——缓存断点前块字节不变(不变量 #9);改稿只动.md,金标准 fixturetests/fixtures/prompt_hashes.json守字节回归。Pydantic 类型永留 Python:SCHEMA_CATALOG[name]→output type是唯一真相源,output_schema由它派生。 - 注册表
SPECS: dict[name, AgentSpec]是内置 agent 的集中名册(name 为唯一主键,SPECS[name]/prompts/<name>.md/SCHEMA_CATALOG[name]三者由 name 对齐,缺一即 fail-fast)。四审受信名锚在显式白名单REVIEW_RESERVED_NAMES={continuity,foreshadow,style,pace}。 - 统一解析入口
SpecResolver.get(name)(packages/skills/ww_skills/spec_resolver.py):内置走纯内存SPECS(零 DB),用户 skill 走SkillRegistry(DB)。同读接口、不同信任级别;内置 name 为保留命名空间,用户 skill 同名在 SkillRegistry 入库校验期即被拒(不在读路径),守不变量 #3。
5.2 编排器(LangGraph 图)
编排器是一张 LangGraph 状态图(非自治 agent loop),节点固定、边确定;用其并行跑四审、用 interrupt(HITL)+ checkpoint 表达"写章→作者裁决→验收"的跨请求暂停-恢复。
# 状态: { project_id, chapter_no, ctx, draft, reviews, decisions }
g = StateGraph(ChapterState)
g.add_node("assemble", lambda s: memory.assemble(s.project_id, s.chapter_no)) # §5.3
g.add_node("write", lambda s: gateway.run(writer_spec, s.ctx, stream=True)) # 纯函数产草稿
# 四审为并行分支(无依赖), 汇入 collect
for spec in (continuity_spec, foreshadow_spec, style_spec, pace_spec):
g.add_node(spec.name, make_review_node(spec)) # 只读, 结构化输出
g.add_node("collect", collect_reviews) # 汇总四审 → 落 chapter_reviews 表(留痕)
g.add_edge("assemble", "write")
for spec in REVIEW_SPECS: # write ─並行→ 四审
g.add_edge("write", spec.name); g.add_edge(spec.name, "collect")
# 验收 = HITL 中断: 图在此 interrupt; 审稿结果已落 chapter_reviews 表, 前端从表读取并裁决;
# 作者 POST /accept 携 decisions(+ 可能改过的终稿) 后, 从 checkpoint 恢复, 进 accept 节点
g.add_node("accept", commit_accept) # 单事务(含「从终稿提炼 digest」): 见 §5.5
graph = g.compile(checkpointer=PostgresSaver(...), interrupt_before=["accept"])
- 确定性:图固定、可单测、可回放(给定输入 + mock 网关,输出确定;checkpoint 落 Postgres)。
- 并行:四审为并行分支汇入 collect;任一失败不阻塞其余,报告标"该审未完成"。
- HITL 恢复 + 真相源边界:用
interrupt_before=["accept"]+ checkpointer 支持"写章请求返回、验收请求恢复"的跨请求流程。正文以chapters表、审稿结果以chapter_reviews表为权威真相源;checkpoint 只存"图走到哪 + 待裁决句柄",不作正文/审稿的真相源——恢复时按chapter_no从领域表重读,避免双真相源(对齐 ADR-2)。 - 编排 vs Agent:图做控制流与事务边界;Agent 节点只做单次认知任务、经记忆库交换(不直连)。
5.2.1 实现现状(单章流程)与多章链图 ← 回写自实现审计 + Chain Workflow 交付(2026-06-23)
单章流程的实现现状(与上面"一张图串全程"的理想形态有别):上图是设计意图;实际单章流程未编译成这一张图——
- 写章:
POST /projects/{id}/chapters/{no}/draft直接gateway.stream()流式产草稿(stream_chapter_draft),不经图。(曾有build_write_graph单节点图但无端点调用,已在多章链图落地时删除;write_node本体保留供链复用。) - 四审:
POST /projects/{id}/chapters/{no}/review编译build_review_graph()做一次性ainvoke(START→四审并行→collect→END),不带 checkpointer。 - 验收:
POST /projects/{id}/chapters/{no}/accept是确定性事务代码run_accept_transaction(晋升终稿→从终稿提炼 digest→裁决留痕,§5.5),不是图节点;其 HITL 闸在 API/DB 层(冲突未裁决→CONFLICT_UNRESOLVED),非 langgraphinterrupt。 - 因此单章流程是线性的,
interrupt/ Postgres checkpointer 在单章流程未实际启用——线性流无需图。
多章工作流链(Chain Workflow)= 本项目首个真正用 LangGraph cyclic 图 + Postgres checkpointer + interrupt 的场景。 设计契约见 docs/design/chain-workflow.md。对标竞品「一键多章」,差异化在「每章过四审、遇冲突才停人」。
# ChainState(只存控制流, 不存正文/上下文/冲突明文): {project_id, chain_key,
# start/last/current_chapter_no, written:[], has_conflicts}
# 节点各自用独立短 session(每章 accept 章原子提交); gateway 经 builder 按节点 session 重建
g = StateGraph(ChainState)
g.add_node("write_chapter", write_chapter) # assemble→收集版写章(gateway.run,非SSE)→落 chapters draft
g.add_node("review_chapter", review_chapter) # 四审→落 chapter_reviews(只读留痕)
g.add_node("decide", decide) # 纯逻辑: 读最近审稿 → has_conflicts
g.add_node("accept_chapter", accept_chapter) # interrupt-on-conflict; 复用 run_accept_transaction + 伏笔扫描
g.add_edge("write_chapter", "review_chapter"); g.add_edge("review_chapter", "decide")
g.add_conditional_edges("decide", ...) # 有冲突→interrupt()暂停交人; 无冲突→直接 accept
g.add_conditional_edges("accept_chapter", ...) # current<last→回 write_chapter(循环); 否则→END
graph = g.compile(checkpointer=AsyncPostgresSaver(...)) # thread_id = job_id
- 跑在
run_job长任务壳里:POST .../chains/{key}/run→202{job_id}+ BackgroundTask;GET /jobs/{id}轮询进度(written);某章四审报冲突→interrupt()暂停(job=awaiting_input)→POST .../chains/runs/{job_id}/resume携裁决Command(resume=decisions)续跑。 - 守真相源边界(同 §5.2):checkpoint 只存控制流位置 + 待裁决句柄;正文/审稿/冲突明文以领域表为权威,resume 时重读
chapters/chapter_reviews(不变量 #5)。 - checkpointer 建表:
setup_checkpointer在 alembic 迁移建 langgraph 检查点 4 表(DDL 只在 migrations/CI,绝不 app-runtime);E2E 用MemorySaver(单进程,零联网)。
5.3 记忆注入与 prompt 组装
def assemble(project_id, chapter_no):
outline_row = outline_repo.get(project_id, chapter_no)
entities = select_relevant_entities(project_id, outline_row) # 确定性, §3.4
# = 显式点名 ∪ 主角常驻 ∪ 近况实体 ∪ (可选)按名匹配
forewin = foreshadow_repo.windows_for(chapter_no)
digests = digest_repo.recent(project_id, k) # 近况摘要
fingerprint = style_repo.latest(project_id)
rules = rules_repo.effective(project_id) # 四级合并
stable_core = serialize_sorted(world_hard_rules + settled_chars + fingerprint + rules)
volatile = serialize_sorted(render_cards(entities) + forewin + digests + outline_row.beats)
return LlmRequest(system=[Block(text=stable_core, cache=True)], input=volatile)
- 缓存断点:stableCore 标
cache:true(断点前),volatile 在后(§4.6)。 - 确定性序列化:排序 + 无时间戳/UUID,保证缓存字节稳定。
- 四级规则合并:
global → genre → style → project,越具体越优先。
5.4 8 个 Agent 的 I/O 契约(对齐 §5.2 花名册)
| Agent | 档位 | reads | writes(经验收) | 输出 schema 要点 |
|---|---|---|---|---|
| worldbuilder | 写手 | projects | world_entities | {entities:[{type,name,rules}]} 硬规则显式 |
| character-gen | 写手 | world_entities, characters | characters | {cards:[{name,role,traits,backstory,arc,speech_tics,tags,relations}]} |
| outliner | 分析 | projects, foreshadow, characters, world_entities | outline | {chapters:[{no,beats,foreshadow_windows}]} |
| writer | 写手 | (注入) | —(产草稿,持久化见下注) | 纯文本流(stream) |
| continuity | 分析 | chapter_digests, characters, world_entities | —(只读) | {conflicts:[{type,where,refs,suggestion}]}(仅冲突;digest 在验收时从终稿另提,见下注) |
| foreshadow-analyst | 分析 | foreshadow | —(只读) | {planted:[...], resolved:[...]} 建议 |
| style-auditor | 轻量(打分)/分析(提取) | style_fingerprint | style_fingerprint(仅学文风提取) | 提取{dims,evidence} / 打分{segments:[{idx,score}]} |
| pace-checker | 轻量 | rules(genre) | —(只读) | {water:[...], hook:bool, beat_map:[...]} |
- 所有非 writer 的 Agent 结构化输出(网关保证,§4.4),便于程序消费与就地裁决。
- writer 不直接写库:产草稿(stream)→自动保存落
chapters(draft)(确定性代码,即时)→验收晋升为accepted新 version(§5.5)。四审writes为空(只读)。 - style-auditor 的写入是例外:发生在学文风流程(写
style_fingerprint),不在写章流水线;写章时它只读指纹做漂移打分。 - digest 从终稿提取,不从审稿草稿:审稿时 continuity 只出冲突;作者裁决/改稿后,验收事务里用终稿另跑一次轻量提炼得
digest再追加chapter_digests(§5.5/§6.1)——否则改稿前的草稿事实会污染真相源。
5.5 验收与状态写回(事务)
- 验收是确定性代码,非 Agent。把作者裁决后的变更单事务落库:
- 章节晋升
accepted新 version; - 从终稿提炼
digest(轻量 LLM 调用,输入是最终验收文本而非审稿草稿)→ 追加chapter_digests; - 应用伏笔状态变更/新登记、更新人物 latest_state、可选加规则;
- 写入本次裁决留痕(
chapter_reviews.decisions)。
- 章节晋升
- 事务保证一致:全成功或全回滚,杜绝"摘要更了但伏笔没更"的半态。
- 写回后触发:伏笔到期扫描(§6.2)、看板/仪表盘缓存失效。
5.6 技能系统运行时(Skill loader + 沙箱)
- 加载:从
skillsregistry 读 AgentSpec(builtin/custom/community),按tier经网关执行(复用 §5.1 机制)。 - 表权限强制(执行点说明):这不是进程级沙箱——Skill 是声明式 prompt,本身不执行代码。强制点在编排/写库层:① 注入时只把声明
reads的表数据喂给该 skill;② 验收/写库代码只把该 skill 产出应用到其声明的writes表(白名单),越权的产出字段丢弃并审计。即"声明式权限 + apply 层白名单",而非隔离运行时。 - 纯声明式:Skill 不含可执行代码,仅 prompt + schema + 权限,杜绝任意代码执行。
- 写库仍经验收 gate:自定义 Skill 产出同样要过作者验收/四审才入库,不开后门。
- 安全细节见 §9.2。
6. 核心模块详细设计 ← PRODUCT_SPEC §4
6.1 长篇一致性
- 摘要提炼:验收时从终稿(作者裁决/改稿后的最终文本,非审稿草稿)提炼
digest(结构化事实),事务追加chapter_digests——避免改稿前的草稿事实污染真相源。 - 实体状态:人物/世界观维护
latest_state;验收按裁决更新(带[CONFLICT]的不自动覆盖)。 - 冲突检测:审稿时 continuity 比对草稿 vs(近况摘要 + 人物卡 + 世界观硬规则),产出结构化冲突清单:
conflict = { type: 性格漂移|能力不符|设定违例|地理矛盾|时间线倒错, where: 本章定位, refs: [冲突来源章/设定], suggestion: 改法 } - 裁决流:前端就地高亮 + 报告条目 → 作者选
采纳改法 / 忽略 / 手改;忽略可沉淀为规则。未决冲突不允许验收(UX §9 状态)。 - 校验清单:参考 Lost in Stories 2026 分类(人物/设定/时间三类)。
- 时间线:MVP 由
chapter_digests推导;P2 落timeline表加速。
6.2 伏笔账本
状态机:
登记 首次照应 完成回收
─────▶ OPEN ──────▶ PARTIAL ─────────▶ CLOSED
│ │
└─────────────┴──▶ OVERDUE (章号 > expected_close_to 且未 CLOSED)
(到期扫描置位; 回收后可转 CLOSED)
- 到期扫描:验收后触发(确定性代码,非 Agent):
current_ch > expected_close_to AND status≠CLOSED → OVERDUE,写回并在看板/报告提醒。 - 新埋/回收建议:审稿时 foreshadow-analyst 检测本章新埋线 / 疑似回收 → 建议,作者确认后登记/改状态。
- 依赖图/窗口:
outline.foreshadow_windows关联章与伏笔;排大纲时提示接近回收窗口的伏笔。 - 看板:按
(project_id, status)索引查询,四泳道 + OVERDUE 强调(UX §6.8)。
6.3 文风模仿
- 提取(一次性,分析档):3–5 样本(≥5 万字)→ 16 维指纹(9 通用 + 7 中文),每维附原文证据,存
style_fingerprint(版本化、可增量合并)。 - 生成约束:写章注入指纹(缓存稳定块)。
- 漂移打分(高频,轻量档):每段对照指纹打相似度分;低于阈值标漂移段。
- 回炉:仅重写漂移段、保留上下文,新旧 diff(UX §8.3)。阈值可配(§10 风险:需校准)。
6.4 节奏引擎
- 模板 DSL(存
rulesgenre 级):黄金三章、章末钩子、爽点密度(每 N 千字一拍)、情绪曲线。 - 检测(pace-checker,轻量档):注水段(信息密度过低/重复铺陈)、章末钩子有无、爽点节拍图、情绪曲线。
- 产出:
{water:[段], hook:bool, beat_map:[...]}→ 报告以"爽点节拍图 ▁▃▅"可视化(UX §6.4)。 - 这是对英文工具的差异化(中文网文模板)。
6.5 角色生成器
- 单/批量:输入一句话需求 + 数量 + 定位 → character-gen(写手档)产出结构化角色卡。
- 防雷同(批量):一次性生成一组时,prompt 注入"已生成卡 + 已有角色",要求差异化定位/性格;可对产出做相似度去重复核。
- 一致性校验接入:入库前由编排器追加一道 continuity 检查(非 character-gen 直接互调,符合 §5.4 数据流),确认不与世界观/力量体系冲突。
- 入库:过校验 → 写
characters(角色行即注入单元,§3.4 渲染成卡片)→ 进入后续确定性选择注入。
7. API 设计 ← PRODUCT_SPEC §6
7.1 REST 约定与错误信封
- 作用域嵌套:章节端点统一
/projects/:id/chapters/:no/...(修正 PRODUCT_SPEC §6 中:no跨项目不唯一的 nit,见文末发现)。 - 鉴权:原型单用户(无登录/归属校验);多租户 + 按
owner_id校验为后续(§9.1)。 - 统一成功:
200/201 { data, meta? }。 - 统一错误信封:
常见码:
{ "error": { "code": "CONFLICT_UNRESOLVED", "message": "...", "details": {} } }NOT_FOUND / VALIDATION / CONFLICT_UNRESOLVED(未决冲突禁验收) / LLM_UNAVAILABLE(回退耗尽) / RATE_LIMITED(多租户上线后补UNAUTHORIZED/FORBIDDEN)。
7.2 端点清单(对齐 §6 + 设置/伏笔/看板)
| Method | Path | 请求 | 响应 | 码 |
|---|---|---|---|---|
| POST | /projects |
立项向导各字段 | project | 201 |
| GET | /projects |
— | 作品列表(含待办徽标) | 200 |
| GET | /projects/:id |
— | 作品详情 | 200 |
| POST | /projects/:id/world/generate |
{需求} |
world_entities(预览) | 200 |
| POST | /projects/:id/characters/generate |
{需求, count?, role?} |
角色卡(预览,待入库) | 200 |
| POST | /projects/:id/characters |
角色卡 | 入库结果 | 201 |
| POST | /projects/:id/style |
样本(mode=update?) |
{job_id}(异步见 7.4) |
202 |
| GET | /projects/:id/style |
— | 最新文风指纹 {dimensions, evidence, version} |
200 (无指纹 404) |
| GET | /jobs/:id |
— | 任务状态/进度/结果 | 200 |
| POST | /projects/:id/outline |
{范围} |
大纲(含伏笔窗口) | 200 |
| POST | /projects/:id/chapters/:no/draft |
{} |
SSE 流草稿 | 200(stream) |
| PUT | /projects/:id/chapters/:no/draft |
{text} |
自动保存草稿(幂等,写 chapters.draft,断流兜底) |
200 |
| POST | /projects/:id/chapters/:no/review |
{draft} |
SSE 流四审报告 | 200(stream) |
| POST | /projects/:id/chapters/:no/accept |
{裁决清单} |
写回结果(将更新清单) | 200 |
| GET | /projects/:id/chapters/:no/reviews |
— | 审稿历史(chapter_reviews) |
200 |
| POST | /projects/:id/chapters/:no/refine |
{段落, 指令} |
回炉重写段(diff) | 200 |
| GET | /projects/:id/foreshadow |
?status= |
伏笔看板数据 | 200 |
| POST | /projects/:id/rules |
{level, content} |
rule | 201 |
| GET | /skills/toolbox |
— | 生成器描述符列表(key/标题/input_fields/ingestable/legacy_route) | 200 |
| POST | /projects/:id/skills/:tool_key/generate |
{brief, chapter_no?, count?, kind?} |
结构化预览(不写库,仅记账) | 200 (未知 key 404/无 provider 503) |
| POST | /projects/:id/skills/:tool_key/ingest |
{world_entities?|scenes?, chapter_no?, acknowledge_conflicts} |
入库结果 | 201 (continuity 冲突 409) |
| GET/PUT | /settings/providers |
凭据/档位配置 | 配置(Key 脱敏) | 200 |
| POST | /settings/providers/test |
{provider} |
连接+能力矩阵 | 200 |
7.3 流式接口(SSE)
draft/review用 Server-Sent Events:网关流式Delta→ 归一 SSE 事件 → 前端打字机渲染。- 事件类型:
token(正文增量)、section(四审分项开始/完成)、conflict(冲突命中)、done、error。 - 断流可重连:前端按已收 token 续接;正文自动保存兜底(不丢稿)。
7.4 异步长任务(BackgroundTasks + jobs 表)
- 长任务:学文风(解析数万字样本)、全书一致性回归扫描、批量群像 → FastAPI BackgroundTasks 在同进程后台跑,先写
jobs行返回{job_id}(202),前端轮询GET /jobs/:id取进度/结果。 - 幂等以
job_id去重;失败置jobs.status=failed可重试。 - 持久性局限 + 缓解:BackgroundTasks 在进程内跑,进程重启/部署会丢任务、
jobs.status卡在running。原型缓解:启动时把所有running僵尸 job 标failed,让用户看到失败并重试(而非进度条永转)。真正的持久重试/跨进程恢复属规模化项——换 arq/Celery(接口不变)。 - 原型不上专门队列。
- 写章/审稿不走后台(要即时流式反馈),走 SSE 同步流。
8. 前端架构 ← UX_SPEC
8.1 应用结构(Next.js App Router;前端独立于后端)
- 路由对齐 UX §3.1 导航:
app/(dashboard)作品库;app/projects/[id]/{write,outline,codex,foreshadow,review,style,rules};app/settings/providers。 - Server Components 取数(列表/看板/设定库只读视图);Client Components 承载编辑器、流式、交互。
- 调用独立 FastAPI 后端(§7 端点),经 OpenAPI 生成的 TS 客户端(
app/lib/api);SSE 用EventSource/fetch流读。无 Next API Routes 业务逻辑(仅可留 BFF 代理/鉴权透传)。
8.2 状态管理
| 状态类别 | 方案 |
|---|---|
| 服务端数据(列表/设定/看板) | React Query / SWR(缓存 + 失效) |
| 编辑器本地态(正文/光标/未保存) | 局部 store(Zustand 或 RSC + local) |
| 流式增量(draft/review) | SSE 订阅 → 增量 reducer |
| 乐观更新(验收/裁决) | 先改 UI、失败回滚 + Toast |
8.3 编辑器与流式渲染
- 正文编辑器:宋体 / 720px / 行高 1.9(UX §2.3);contentEditable 或 ProseMirror/TipTap(富文本 + 锚点标注)。
- 流式打字机:SSE
token逐字淡入 + 朱砂光标;尊重prefers-reduced-motion(关动效)。 - 冲突标注:
conflict事件 → 正文对应区间挂朱砂波浪线 + 悬浮卡 + 报告联动跳转。 - 自动保存:debounce 持续保存草稿(写
chapters当前 version),显示"保存于 hh:mm"。
8.4 设计 token 落地
- UX §2 的色板/字体/间距 → CSS 变量(
--paper,--ink,--vermilion…)+ Tailwind theme。 - 纸感主题集中在
:root;预留[data-theme="night"]夜读模式扩展位(UX §2.1 备注)。 - 组件库(UX §7)一一实现:AppShell / Editor / ReviewCard / ForeshadowCard / TierRouter / ProviderRow …
9. 横切关注点 ← PRODUCT_SPEC §10 / §5.5
9.1 鉴权与多租户隔离(原型:单用户)
- 原型阶段:单用户,无登录/无归属校验;
users表与projects.owner_id保留为接缝(可写死一个 stub 用户),暂不强制。 - 多租户化(后续):接入 Auth(邮箱/OAuth)→ 所有 Repository 强制
project_id + owner_id行级隔离 → 越权返回FORBIDDEN+ 审计;再远期多人协作预留project_members。 - 因 Repository 已统一封装表访问,多租户化时只在该层加归属校验,不动业务逻辑。
9.2 安全
| 面 | 措施 |
|---|---|
| 密钥 | 提供商 Key 加密存储(KMS/加密列);前端永不回显;仅后端解密用于调用 |
| 用户 Skill(不可信输入) | 纯声明式(无代码执行);reads/writes 表权限白名单强制;产出经验收 gate(§5.6) |
| 提示注入 | 用户/作品文本以数据注入,不与系统指令混层;结构化输出 + 校验抑制越权产出;Skill prompt 不得提权 |
| 输入校验 | 所有端点 Pydantic 校验(系统边界),失败 VALIDATION |
| 数据外发 | LLM 调用即把作品内容发往第三方——UX §6.10 标境内/境外,由作者/运营按合规取舍 |
| 注入/越权审计 | 关键操作(验收/改设定/Key 变更/Skill 越权拦截)留审计日志 |
9.3 可观测性
- 结构化日志(structlog):请求、编排步骤、网关调用(provider/model/usage/fell_back/cache_read)。
- 追踪(OpenTelemetry):一次写章 = 一条 trace,跨"检索→writer→四审→验收"span,定位慢点。
- 指标:四审耗时、缓存命中率、回退率、每作品成本;驱动 UX §6.10 成本看板与告警。
9.4 韧性与配置
- 韧性:网关回退/熔断/重试(§4.5);SSE 断流可续;正文自动保存兜底;长任务可重试幂等。
- 配置:
config模块集中——提供商注册、档位默认映射、env 解析、特性开关(P1/P2 功能灰度)。
10. 部署与运维 ← PRODUCT_SPEC §3.1 / §10
- 环境:dev / staging / prod 三套;env 与密钥分环境管理。
- 部署拓扑:前端 Next.js + 后端 FastAPI + Postgres;常驻服务(Fly.io/Render + 托管 PG,或 Docker 自托管)——因后端有 BackgroundTasks 长任务,避免 serverless 杀长函数。后端无状态可水平扩。
- 数据库:托管 Postgres(无需 pgvector 扩展;P2 引入向量时再开);定时备份 + PITR;Alembic 迁移随发布执行、CI 校验。
- 扩展性/配额:写章并发受 LLM 限流约束——按作品速率配额;网关侧聚合各 provider 限流(429 退避 + 回退)。规模化再抽独立 Worker(arq/Celery)削峰。
- 发布:CI 跑测试 + 迁移校验 + 类型检查;蓝绿/滚动;DB 迁移向后兼容(先加列后用)。
11. 测试策略
对齐团队规范(≥80% 覆盖,单元/集成/E2E 三类,TDD):
| 层 | 范围 | 要点 |
|---|---|---|
| 单元 | 领域逻辑/状态机/纯函数 | 伏笔状态机、四级规则合并、prompt 组装确定性、档位解析 |
| 集成 | Repository + DB、LangGraph 图 + mock 网关、端点 | 验收事务原子性、HITL 恢复、SSE 流 |
| 契约 | LLM 结构化输出 schema | 每个 Agent 的 outputSchema 用录制样例做契约测试;适配器能力矩阵测试 |
| E2E | 关键流程 | 写一章闭环(写→审→裁决→验收)、生成群像、切换提供商 |
- LLM mock:网关可注入 mock provider(返回固定/录制响应),使编排器/Agent 可确定性测试,不烧真实 token。
- 降级/回退测试:模拟 429/不支持结构化输出,验证降级与回退路径。
- TDD:新模块先写测试(RED→GREEN→重构)。
12. 实施路线(架构视角) ← PRODUCT_SPEC §9
按依赖关系排架构落地顺序(与产品 M1–M5 对齐):
| 里程碑 | 架构交付 | 依赖 |
|---|---|---|
| M1 | db(建全部表+迁移,含 rules) → llm_gateway(单 provider 起 + 档位) → memory(确定性选择+组装) → orchestrator(写章) → API(立项/写本章/SSE) → 前端 AppShell+工作台 + 设置页(至少一个 provider) |
无 |
| M2 | continuity Agent + review SSE + 验收事务 + 冲突裁决 UI | M1 |
| M3 | 伏笔状态机 + 到期扫描 + 看板 + outliner 伏笔窗口 + pace-checker | M2 |
| M4 | 文风提取(异步任务) + style-auditor 双轨 + 回炉 | M1 网关 |
| M5 | worldbuilder/character-gen 生成 + 多 provider 回退/降级完善 + Skill 运行时 + 规则/命令面板 | M2–M4 |
网关的多 provider/回退/降级在 M1 先打接口与单 provider,M5 补齐多家与韧性——避免一开始过度工程。
附录 A · ADR 关键决策记录
见 §1.2 摘要表;正式实现期每条 ADR 单独建档(背景/选项/决策/后果)。核心十条:模块化单体、库为真相源、自建网关、声明式 Agent、写章纯函数、Postgres 单库(确定性选择/无向量)、网关统一结构化输出、前端 Next + 后端 FastAPI 分离、BackgroundTasks 长任务、LangGraph 编排。
附录 B · ARCHITECTURE ↔ PRODUCT_SPEC 覆盖矩阵
| PRODUCT_SPEC 章节 | 对应 ARCHITECTURE |
|---|---|
| §1 问题 / §1.5 哲学 / §2 目标 | §1.1 NFR / §1.2 决策 |
| §2.5 功能总览 | §12 路线(按 P0–P2 落地) |
| §3.1 技术栈 | §2 技术栈与工程结构 / §10 部署 |
| §3.2 数据模型 | §3 数据架构(完整 DDL) |
| §3.3/§3.4/§3.5 网关/直连/缓存 | §4 LLM 网关架构 |
| §3.6 编排示意 | §5.2 编排器 |
| §4 四大模块 + 角色生成 | §6 核心模块详细设计 |
| §5 多 Agent | §5 Agent 编排引擎 |
| §6 接口 | §7 API 设计 |
| §7 规则积累 | §5.3 四级规则合并 + §7 rules 端点 |
| §8 差异化 | (产品论证,不在架构展开) |
| §9 实施路线 | §12 实施路线(架构视角) |
| §10 风险 | §9 横切关注点(安全/韧性/合规) |
| UX_SPEC | §8 前端架构 |
附录 C · 术语表
| 术语 | 含义 |
|---|---|
| 档位(tier) | 能力等级(写手/分析/轻量),网关映射到 provider+model |
| 适配器 | 把统一 LlmRequest 翻译成某厂商 API 的模块 |
| 真相源 | 数据库——Agent 间唯一交换媒介 |
| 四审 | continuity/foreshadow/style/pace 四个并行质检 Agent |
| 验收 gate | 作者裁决后的事务性写回,AI 产出入库唯一入口 |
| 稳定内核 | prompt 中可缓存的稳定前缀(世界观硬规则+已定型角色+文风指纹) |
待回写规格的发现(已同步三份文档一致)
下列在架构下沉中暴露的上游缺口已回写 PRODUCT_SPEC / UX_SPEC,三份文档现已一致:
- ✅ API 作用域:章节端点统一为
/projects/:id/chapters/:no/...(PRODUCT_SPEC §6 + UX §3.2 已改)。 - ✅ 向量检索砍除:原型改确定性记忆选择(§3.4),删除
embedding列/HNSW;PRODUCT_SPEC §3.2 已注明"向量检索为 P2,届时再加 embedding 列 + 嵌入服务"。 - ✅ 回炉端点:PRODUCT_SPEC §6 已增
POST /projects/:id/chapters/:no/refine(本架构 §7.2)。 - ✅ users 表:PRODUCT_SPEC §3.2 已补
users与projects.owner_id(本架构 §3.1 运营表)。