feat(backend): AI 反问澄清预检端点——refine 侧结构化 clarify(WFW-9 M1,路线A 两阶段)

润色「再沟通」意见含糊时先反问给选项(路线A:问题走独立非流式 JSON 预检端点,正文仍走
既有 refine 一字不改)。新增:
- ClarifyDecision/ClarifyQuestion/ClarifyOption 结构化 schema(既有 output-schema 处,供
  producer 与端点共用);clarify_refine.md 教条(含糊→need_clarification+≤1问+2–4锚定选项+
  自由输入;明确→verification 放行;防循环);注册 clarify_refine_spec(analyst 档,#24)+
  SCHEMA_CATALOG + 重生成金标准。
- clarify_node:build_clarify_request 纯函数(缓存前缀不含易变) + run_clarify(gateway.run
  结构化,判别/校验失败确定性回退 need_clarification=false,只读不写库)。
- POST /projects/{id}/chapters/{no}/refine/clarify → ClarifyDecision(analyst 网关,404/503,
  末尾 commit 记账)。**既有 refine 端点/RefineRequest/Response 完全未改**。
门禁绿:ruff/mypy 227/pytest 900(+test_clarify_spec/_node/style clarify)/alembic 无漂移。
This commit is contained in:
Yaojia Wang
2026-07-08 08:47:13 +02:00
parent fb5caa3d89
commit 1652ad9d20
15 changed files with 697 additions and 5 deletions

View File

@@ -22,10 +22,10 @@ from typing import Annotated, Any
from fastapi import APIRouter, BackgroundTasks, Depends, Request, Response
from sqlalchemy.ext.asyncio import AsyncSession
from ww_agents import refiner_spec, style_extract_spec
from ww_agents import ClarifyDecision, clarify_refine_spec, refiner_spec, style_extract_spec
from ww_core.domain import JobRepo, ProjectRepo, StyleFingerprintWriteRepo
from ww_core.domain.style_repo import SqlStyleFingerprintWriteRepo
from ww_core.orchestrator import run_style_extraction
from ww_core.orchestrator import run_clarify, run_style_extraction
from ww_db import get_session
from ww_llm_gateway import Gateway
from ww_llm_gateway.types import Block, LlmRequest, Scope
@@ -34,6 +34,7 @@ from ww_shared import AppError, ErrorCode, ErrorEnvelope
from ww_api.logging_config import get_logger
from ww_api.schemas.style import (
DimensionEntry,
RefineClarifyRequest,
RefineRequest,
RefineResponse,
StyleFingerprintResponse,
@@ -45,6 +46,7 @@ from ww_api.services.foreshadow_scan import SessionFactory
from ww_api.services.job_runner import run_job
from ww_api.services.project_deps import (
build_gateway_for_tier,
get_clarify_gateway,
get_job_repo,
get_project_repo,
get_refine_gateway,
@@ -62,6 +64,7 @@ JobRepoDep = Annotated[JobRepo, Depends(get_job_repo)]
StyleWriteRepoDep = Annotated[StyleFingerprintWriteRepo, Depends(get_style_write_repo)]
StyleExtractGatewayDep = Annotated[Gateway, Depends(get_style_extract_gateway)]
RefineGatewayDep = Annotated[Gateway, Depends(get_refine_gateway)]
ClarifyGatewayDep = Annotated[Gateway, Depends(get_clarify_gateway)]
SessionDep = Annotated[AsyncSession, Depends(get_session)]
SessionFactoryDep = Annotated[SessionFactory, Depends(get_session_factory)]
@@ -238,3 +241,59 @@ async def refine_segment(
has_instruction=body.instruction is not None,
)
return RefineResponse(original=body.segment, refined=resp.text)
@router.post(
"/{project_id}/chapters/{chapter_no}/refine/clarify",
responses={
404: {"model": ErrorEnvelope, "description": "项目不存在"},
503: {"model": ErrorEnvelope, "description": "LLM 不可用"},
},
)
async def clarify_refine(
project_id: uuid.UUID,
chapter_no: int,
body: RefineClarifyRequest,
request: Request,
project_repo: ProjectRepoDep,
gateway: ClarifyGatewayDep,
session: SessionDep,
) -> ClarifyDecision:
"""润色预检澄清WFW-9 M1 路线A 两阶段之「问题」阶段):非流式 JSON只判不改。
独立于既有 refine 端点——只判「作者这条再沟通意见清不清楚」,含糊则反问 1 问 + 给选项,
清楚则给确认语放行;**绝不改正文**(正文仍走 refine 端点。analyst 档(不变量 #2
项目不存在 → 404无凭据 → 503dep 解析阶段拦下)。**只读不写库**(不变量 #3
末尾 `commit()` 让网关 usage_ledger 落库add-only否则记账静默丢失同 refine 纪律)。
判别/校验失败在 `run_clarify` 内确定性回退 `need_clarification=false`(不阻塞润色)。
"""
request_id = getattr(request.state, "request_id", None)
project = await project_repo.get(STUB_OWNER_ID, project_id)
if project is None:
raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}")
context = _build_refine_input(body.segment, body.instruction or None)
decision = await run_clarify(
clarify_refine_spec, # 不变量 #2analyst 档,不传 model
context=context,
gateway=gateway,
user_id=STUB_OWNER_ID,
project_id=project_id,
)
# 提交边界:只读不写业务表,但网关 ledger add-only → 端点末尾 commit否则记账丢失
await session.commit()
log.info(
"refine_clarify_done",
project_id=str(project_id),
chapter_no=chapter_no,
request_id=request_id,
segment_len=len(body.segment),
instruction_len=len(body.instruction),
need_clarification=decision.need_clarification,
question_count=len(decision.questions),
)
return decision