Also note that the seven architecture amendments predate the plan, tighten T12 to independent implementation layers, and add fallback guidance for read-only reference protocol imports.
26 KiB
M1 核心里程碑设计:公共签名冻结与治理栈落地
状态: Claude 自审 ✅ → 独立审查 ✅(全新上下文 subagent 顶替 Codex——本机 codex CLI 损坏;6 Issues + 4 建议已全部核验修订)→ 待人类门。依据: ARCHITECTURE.md D1–D14 / ROADMAP §2 / migrations/ 三份;签名证据来自 2026-07-20 对 reference/ 三项目的逐字提取(文中
文件:行号均指reference/下路径)。 范围: M1 全部交付物(ROADMAP §2 七步),含 2026-07-20 人类拍板:多源完整行为进 M1;GovDoc 与 Video-Tree 双冒烟;Redis 集成测试用实验室远程实例。 本文档冻结的内容:types.py/errors.py/ports.py公共签名、配置键名、五个开放设计轴的取舍。与 ARCHITECTURE.md 冲突处在 §13 列为反哺修订,经人类批准后先改 ARCHITECTURE 再实施。
1. 目标与非目标
M1 出口:GovDoc 与 Video-Tree 用 GatewayClient.from_env() + chat() 替代各自 GovernedLLMClient 跑通一次真实治理调用;make ci 绿、覆盖 ≥80%。非目标:Redis 限流/熔断后端、背压 stall、Postgres 遥测、pricing、OCR、音频(M2/M3)。
2. 核心类型冻结(types.py)
2.1 LLMResponse
前 11 个字段与两参考项目逐字一致(VT core/types.py:12-29 ≡ GovDoc docagent_core/types.py:8-25),顺序不变、无默认值——三项目测试中按位置构造 fake 的代码零改动。新增字段全部带默认值(ARCHITECTURE §5.1 稳定性约定①):
@dataclass(frozen=True)
class LLMResponse:
content: str; thinking: str; model: str; provider: str
prompt_tokens: int; completion_tokens: int; latency_ms: int
ttft_ms: float | None; max_inter_token_ms: float | None
cache_hit: bool; call_id: str
# —— 库新增(只增不删,必带默认值)——
source_name: str = ""
cost: float | None = None
usage_source: str = "measured" # "measured" | "estimated"
structured_data: Any | None = None # D14 阶梯通过后的解析产物
structured_data不参与缓存序列化(pydantic 实例不可 JSON 往返);缓存命中且调用方传了structured时,由 CacheMW 复用装配时注入 StructuredMW 的同一 strategy 实例对缓存content重跑阶梯②③(零网络)再填充——schema 变更后旧缓存自动重校验,失败按未命中处理并记 warning(命中路径不经过 StructuredMW,该职责必须显式落在 CacheMW)。- 缓存反序列化防御:未知字段过滤、缺失新字段吃默认值(版本偏移不炸)。
2.2 chat() 签名与 ChatRequest
chat() 按 ARCHITECTURE §5.2 定稿,唯一细化是 structured 的类型:
async def chat(
self, messages: list[dict[str, Any]], *,
session_id: str | None = None,
parent_call_id: str | None = None,
cache_salt: str | None = None,
cache_namespace: str | None = None,
structured: type[BaseModel] | Literal["json"] | None = None,
stream: bool = True,
) -> LLMResponse: ...
洋葱内部流转 ChatRequest(frozen dataclass):上述全部参数 + overlay: dict(StructuredMW/注册表写入的请求体叠加项,如 response_format、thinking 参数)。中间件用 dataclasses.replace() 派生新请求,不做原地修改(见 §4.1 方案 A1)。
2.3 SourceConfig(CHS config.py:43-83 超集)与辅助类型
| 字段 | 类型/默认 | 说明 |
|---|---|---|
| name / provider / base_url / api_key / model | str,必填 | provider 必须是注册表键(§7) |
| max_concurrency / rpm / tpm | int = 0 | 0 = 该闸不启用(VT/GovDoc 迁移无 TPM 配置) |
| est_tokens | int = 0 | TPM 预扣 + usage 缺失兜底;tpm > 0 时必填 > 0(装配期校验)。默认 0 时 usage 缺失兜底落 0 并标 estimated——与 §9 行 10 不矛盾:放弃的是 VT"缺失填 0 且不标注"的静默行为 |
| timeout_s | float,必填 | 须 ≤ permit 租约 TTL(ARCH §7.3 装配守卫) |
| ttft_timeout_s / inter_token_timeout_s | float | None = None | 不变式 0 < inter < ttft < timeout_s(CHS config.py:66-82) |
| enable_thinking | bool | None = None | 三态:None=不注入(模型默认)/ True=注入开启 / False=注入关闭——同时覆盖 VT"注入开启"(llm.py:130-144)与 CHS"注入关闭"(invokers.py:230-238)两种现状 |
| missing_done | str = "retry" | SSE 缺 [DONE] 语义,见 §6 |
| trust_env | bool = True | 代理绕行(VT ocr.py:46 教训,M3 OCR 复用) |
2.4 client 公共面与装配签名
| 签名 | 说明 |
|---|---|
async def aclose(self) -> None + __aenter__/__aexit__ |
生命周期 API(ARCH §5.1 约定③);aclose 幂等,关闭各源 httpx client 与遥测连接 |
from_env(scope: str = "LLM", *, limiter: RateLimiter | None = None, breaker: ProviderGate | None = None, cache: CacheBackend | None = None, telemetry: TelemetryRecorder | None = None) -> GatewayClient |
classmethod;None 项按 env 构建私有实例,显式传入即共享——多逻辑角色共享全局并发/RPM/TPM 闸(VT TREE_BUILD_API_CONCURRENCY 语义,VT 迁移缺口 R5)= 业务侧自建一个 InMemoryLimiter 传给多次 from_env;全局闸计数在 limiter 实例内,单源闸按 source name 键 |
from_settings(settings: GatewaySettings, *, limiter=..., breaker=..., cache=..., telemetry=...) -> GatewayClient |
同上,配置对象替代环境读取(GatewaySettings 为 pydantic-settings 模型,聚合 §8 全部键) |
| 构造函数 | 全量依赖注入(middlewares/transport/sources/registry/后端全显式),测试与高级用户路径 |
async def gather_bounded(aws: Iterable[Awaitable[T]], *, concurrency: int) -> list[T] |
模块级便利函数;语义 = asyncio.gather 默认行为(结果保序、首个异常上抛)+ semaphore 并发上限;替代 VT 手搓样板 |
其余:Usage(prompt_tokens, completion_tokens, usage_source);SourceStats(inflight, rpm_used, tpm_used)(CHS ports.py:396);TransportResult(transport 返回的内部类型:content/thinking/两 token 数/usage_source/ttft_ms/max_inter_token_ms/raw)。韧性配置四件套沿用 CHS 字段:RetryPolicy(max_attempts, backoff_base_s, backoff_max_s)、BreakerConfig(fail_threshold, cooldown_s, probe_ttl_s)、BackpressurePolicy(stall_window_s, poll_interval_s)(M1 仅 poll 用,stall 判定 M2)、GlobalLimits(max_concurrency, rpm, tpm)。
max_attempts 语义统一:= 总尝试次数(含首次)。VT range(max_retries) 同义;CHS fails > max_attempts 为失败数上限,迁移映射时 +1(记入 CHS 迁移文档)。
3. 错误模型冻结(errors.py)+ §6.1 勘误
class PolyGatewayError(Exception):
def __init__(self, message: str, *, source_name: str | None = None,
status_code: int | None = None, operation: str | None = None): ...
class TransientError(PolyGatewayError): # + retry_after_s: float | None = None
class SourceDeadError(PolyGatewayError): ...
class RequestRejectedError(PolyGatewayError): ...
class ResultInvalidError(PolyGatewayError):
# + raw_text: str, repair_error: str | None, validation_errors: tuple[str, ...]
class GatewayUnavailableError(PolyGatewayError):
# + scope: str, reason: str, retry_after_s: float, per_source_reasons: dict[str, str]
class CircuitOpenError(GatewayUnavailableError): ... # reason 恒 "circuit_open"
class AllSourcesExhausted(GatewayUnavailableError): ...
class GovernanceBackendError(PolyGatewayError): ... # 限流/熔断后端故障:报错不放行
基类构造形态承 CHS ProviderError(errors.py:82);GatewayUnavailableError.retry_after_s 为非可选 float(CHS ProviderUnavailableError:155-176 同),业务侧 arq 延期重投(workers/tracking.py:406-428)只需 catch 基类。全部类自 polygateway 顶层导出(§5.1 约定②)。
勘误(反哺 ARCHITECTURE §6.1):该节称 reason 承接"CHS 的 7 种:circuit_open/retry_exhausted/stalled/quota_exhausted/no_sources/backpressure_timeout/probe_pending",但 CHS errors.py:143-153 实际值域是 {network_error, timeout, rate_limited, source_dead, circuit_open, retry_exhausted, stalled}——前者混入了未曾存在的值。重组为两层值域:
| 层 | 值域 |
|---|---|
scope 级 reason |
circuit_open / retry_exhausted / stalled(M2 启用)/ quota_exhausted(fail-fast 限流满)/ no_sources |
per_source_reasons 值 |
network_error / timeout / rate_limited / source_dead / circuit_open / cooldown |
4. 端口冻结(ports.py,全部 @runtime_checkable Protocol)
4.1 设计轴 A:中间件上下文形态
Middleware 协议:async def __call__(self, request: ChatRequest, call_next: CallNext) -> LLMResponse,CallNext = Callable[[ChatRequest], Awaitable[LLMResponse]]。
| 方案 | 权衡 |
|---|---|
| A1 不可变 ChatRequest + 派生传递(推荐) | 中间件互不污染;衍生请求(结构化反馈重问、overlay 注入)显式 replace();观测数据由 LLMResponse/异常携带,逐次遥测在 RetryMW 内部(它本就拥有尝试编排的全部信息) |
| A2 可变 CallContext 贯穿 | 遥测取数方便,但任何层都能偷改状态,重问循环下请求突变不可追踪;三项目 God-method 的病根之一 |
| A3 contextvars 传递 | 隐式全局,违反纯 asyncio 中立铁律;直接否决 |
遥测调用点收敛:内部组件 TelemetryEmitter(包装 TelemetryRecorder,从 request+结果/异常组装 18 字段)注入 TelemetryMW 与 RetryMW,是全库唯一调用 record_llm_call 的地方(铁律执行点)。
4.2 设计轴 B:限流端口粒度
| 方案 | 权衡 |
|---|---|
| B1 端口在 limiter 层,契约 = CHS 形态(推荐) | try_acquire/acquire/source_stats/mark_progress/progress_age_s + Permit(release, settle)(CHS ports.py:494,507);算法在各后端内实现,契约测试一套两后端共跑(CHS tests/contracts_limiter.py 5 项扩充) |
| B2 端口在存储原语层(incr/zadd 抽象),算法一份在中间件 | Redis 六道闸必须单条 Lua 原子执行(§10 原子性),原语端口无法原子组合;否决 |
| B3 无端口,duck-type 两个类 | 失去 import-linter 可执法的接缝;否决 |
D3"算法只有一份"在限流处的准确含义 = 语义契约与契约测试只有一份(ARCHITECTURE §7.3 已如此表述);熔断/重试/退避则是字面的算法一份。settle()/release() 幂等、permit 带 TTL 租约、TPM 预扣 est_tokens 结算多退少补,全部进契约测试。
4.3 设计轴 C:熔断端口
| 方案 | 权衡 |
|---|---|
C1 采 CHS gate 契约(推荐):try_enter(source, owner) -> Decision、record_success(entry)、record_failure(entry, reason, force_open)、release_probe(entry)、retry_after_s(sources)(provider_gate.py:105-169) |
半开探针租约(TTL 防探针死锁)与 epoch fencing(防旧世代写回)在契约内;M2 的 Redis 版零适配移植;内存版以 VT breaker.py 状态机为蓝本按此契约重写(时钟构造注入) |
C2 采 VT 简单 API(is_open/record_failure/force_open/record_success) |
内存版零改动,但探针 owner 与 epoch 无处安放,M2 Redis 版被迫另立 API → 中间件写两套分支,违反算法一份 |
| C3 两套 API 并存各配各的 | 同上,直接否决 |
Decision/Update 数据类照 CHS ports.py:405-455 冻结(含 __post_init__ 一致性校验):Decision(source_name, allowed, state, epoch, is_probe, probe_owner, retry_after_s)、Update(applied, state, epoch, failure_count, retry_after_s);时间单位一律秒(与 cooldown_s、GatewayUnavailableError.retry_after_s 一致;CHS Redis 实现内部的毫秒换算是后端私事,不进契约)。有效阈值 max(configured, concurrency*2) 由装配层自动计算(三项目 .env 注释的手动约定入库)。
4.4 其余端口(无争议,直接冻结)
| 端口 | 签名要点 | 出处/理由 |
|---|---|---|
Transport |
async def complete(*, messages, source: SourceConfig, stream: bool, overlay: dict, call_id: str) -> TransportResult;内部含看门狗、SSE 解析、ARCH §6.2 错误翻译。看门狗活性口径:任何增量(content 或 reasoning_content)都算 token——ttft = 首个任意 token、思考流刷新 inter_token 计时(CHS invokers.py:55-79;CHS 迁移约束 R1,防 thinking 模型被看门狗误杀) |
D2;overlay 承接注册表 thinking 参数与 NativeSchema 的 response_format |
CacheBackend |
async get(key) -> str | None / async set(key, value, ttl_s) -> None;key 公式上移至 CacheMW(算法一份),后端是笨 KV |
VT redis_cache.py 的 key 构造从类中析出;内存版 dict+TTL 供测试与无 Redis 场景 |
TelemetryRecorder |
async record_llm_call(*, <18 字段>),keyword-only。字段冻结:call_id, parent_call_id, session_id, model, provider, source_name, messages, response, thinking, prompt_tokens, completion_tokens, usage_source, latency_ms, ttft_ms, max_inter_token_ms, cache_hit, error, cost(ARCH §7.8) |
唯一更名:旧 Protocol 的 model_name → model(VT 迁移文档 §8 已记录,grep 实测两项目均只写不读 telemetry.db,更名零风险);表名 llm_calls 保留,旧 db 留档 |
SourceSelector |
def order(sources, stats) -> list[SourceConfig];首发 round_robin / least_inflight |
CHS selector.py:12 逐字 |
StructuredOutputStrategy |
def request_overlay(schema: dict | None) -> dict + def parse(text: str) -> Any(失败抛 ResultInvalidError) |
双实现见 §5 |
| 时钟/随机注入 | 有状态组件构造收 now: Callable[[], float]、sleep、rng(CHS governance.py:85-89 形态) |
P6 可测试性 |
5. 设计轴 D:结构化输出阶梯落位(D14 细化)
| 方案 | 权衡 |
|---|---|
D1 独立 StructuredMW,层序 遥测→缓存→结构化→重试→transport(推荐) |
重问 = 再次调用 call_next(天然照过限流/熔断门、逐次遥测);缓存在其外 → 只缓存阶梯通过的最终响应(D14"不固化坏结果"自动成立);反馈改 messages 经 replace() 显式派生 |
| D2 循环写在 client.chat() 里(洋葱之上) | 重问绕过遥测/缓存中间件,需手工补记账;循环与洋葱两处编排,回到 God-method |
| D3 策略塞进 transport | transport 管协议不管语义;重问跨 attempt,与"transport 无重试"的 D2 职责划分冲突 |
细则(D14 留给本设计的三件事):
- 反馈模板(库内常量,英文、零业务词):重问 = 原 messages +
assistant(原始坏输出)+user("Your previous reply was not valid JSON matching the required schema. Errors: {errors}. Reply with ONLY the corrected JSON object.")。{errors}取修复/校验错误前 3 条、每条截断 200 字符(防 prompt 膨胀)。 - 策略升级:首次尝试用装配时策略(注册表声明
supports_native_schema则 NativeSchema,否则 JsonRepair);重问一律叠加 NativeSchema(若支持)——修复失败说明 prompt 约定不够,升级到协议约束。 - 归一化钩子:VT/GovDoc 的
_normalize_action(loop.py:379-399,DeepSeek 平铺参数收拢)是面向特定业务 schema 的归一化,入库即违反零业务假设。库提供JsonRepairStrategy(normalize: Callable[[Any], Any] | None = None)注入点,业务侧自带归一化函数;ResultInvalidError携带的raw_text保证兜底可行。
structured="json" 档 = 仅阶梯②(围栏剥离 → json_repair → json.loads),产物为 dict/list,失败抛 ResultInvalidError 不重问——恰好继任 CHS 被跨模块偷 import 的 _extract_json_object(extractors.py:32-51,其"失败即抛、不修复"语义由 max_structured_retries=0 默认覆盖)。max_structured_retries 默认 1,装配级配置;CHS 迁移设 0。
6. 设计轴 E:SSE 截断语义
现状分歧:VT 缺 [DONE] 一律 _SseAnomaly("truncated_no_done") 重试(llm.py:585-586);CHS 区分 early_eof(零内容,retry)/ missing_done(有内容,打捞)(invokers.py:307-313);GovDoc 无检查(缺陷)。
| 方案 | 权衡 |
|---|---|
| E1 一律 strict 重试 | 语义最干净,但 CHS 迁移丢打捞行为(迁移文档常驻约束:旧版行为不得隐式丢弃) |
| E2 一律打捞 | 截断响应可能进缓存被固化,违反缓存防毒化精神 |
E3 per-source missing_done: "retry" | "salvage",默认 retry(推荐) |
默认严格(防缓存固化截断);CHS 迁移配 salvage 保留原行为;early_eof(零内容)恒 retry 不可配 |
打捞路径强制 usage_source="estimated"(usage 帧通常随 [DONE] 前的末帧丢失)。
7. provider 注册表(providers.py)
@dataclass(frozen=True)
class ProviderProfile:
name: str
thinking_on: dict # enable_thinking=True 时并入请求体
thinking_off: dict # enable_thinking=False 时并入
strip_think_tags: bool # 响应 content 中 <think> 剥离(qwen)
supports_native_schema: bool = False
首发注册:qwen({"enable_thinking": True} / {"enable_thinking": False}、剥 think 标签)、deepseek({"thinking": {"type": "enabled"}} / {"thinking": {"type": "disabled"}})、openai(全空,基线)。reasoning_content 增量提取是 OpenAI 兼容 SSE 的通用行为,归 transport 不进 profile。查找按 SourceConfig.provider 精确匹配,未注册即装配期报错(消灭子串猜测);register_provider 公开,新 provider 一个条目零核心改动(D11)。形态定稿(2026-07-20 计划审查修订): 纯函数 register_provider(profile, *, base: Mapping | None = None) -> dict[str, ProviderProfile],返回 base(缺省模块级不可变 DEFAULT_PROFILES)+ 新条目的新表;client/from_env 收 registry: Mapping | None = None——无可变全局状态(铁律)。
8. 配置面定稿(from_env 键名全集)
| 组 | 键 | 说明 |
|---|---|---|
| 多源 | {SCOPE}__{PROVIDER}__{N}__{FIELD},FIELD ∈ CHS 全集(config.py:95-104)+ ENABLE_THINKING / TTFT_TIMEOUT_S / INTER_TOKEN_TIMEOUT_S / MISSING_DONE / TRUST_ENV |
聚合为 list[SourceConfig] |
| scope 全局闸 | {SCOPE}__GLOBAL__MAX_CONCURRENCY / RPM / TPM |
CHS 同名 |
| per-scope 韧性 | {SCOPE}__RETRY__MAX_ATTEMPTS / BACKOFF_BASE_S / BACKOFF_MAX_S;{SCOPE}__BREAKER__FAIL_THRESHOLD / COOLDOWN_S / PROBE_TTL_S;{SCOPE}__BACKPRESSURE__STALL_WINDOW_S / POLL_INTERVAL_S;{SCOPE}__SELECTOR;{SCOPE}__QUOTA_FULL(wait/fail_fast) |
CHS 键名 + 新增 PROBE_TTL_S、QUOTA_FULL |
| 平铺简写(单 scope) | LLM_TIMEOUT / LLM_MAX_RETRIES / LLM_RETRY_BASE_DELAY / LLM_RETRY_MAX_DELAY / LLM_CIRCUIT_BREAKER_THRESHOLD / LLM_CIRCUIT_BREAKER_COOLDOWN / LLM_TTFT_TIMEOUT / LLM_INTER_TOKEN_TIMEOUT |
VT/GovDoc 现有键零改名迁移;与 scope 键并存时 scope 键优先(ARCHITECTURE §9) |
| 装配选择 | PGW_LIMITER_BACKEND / PGW_BREAKER_BACKEND(memory|redis,M1 仅 memory)、PGW_CACHE_BACKEND(redis|memory|none)、PGW_TELEMETRY_BACKEND(sqlite|none)、PGW_TELEMETRY_SQLITE_PATH、PGW_CACHE_NAMESPACE(缓存启用时必填)、PGW_CACHE_TTL_S(必填 >0)、PGW_STRUCTURED_MAX_RETRIES |
缺关键键直接报错,严禁默认值兜底 |
| 复用 | REDIS_URL |
VT 同名 |
from_env(scope) 按 scope 装配单 client;多逻辑角色 = 业务侧对每个角色调一次 from_env(scope=...),共享状态后端经 §2.4 的显式注入参数传入(ARCH §7.7,VT 迁移 R5)。VT 的 SEARCH_LLM_MODEL 旧式键由其迁移文档映射为 SEARCH__...,库不做旧键兼容层。
9. 旧版行为审计(替换 VT/GovDoc GovernedLLMClient 栈 + CHS 治理循环)
| # | 旧行为(出处) | 处置 |
|---|---|---|
| 1 | chat() cache_salt(仅 VT,llm.py:274;调用点 loop.py:336) |
保留(GovDoc 无此参 = 超集兼容) |
| 2 | 熔断半开单探针(仅 VT breaker.py:45-48 内存 flag;GovDoc 每调用放行) |
替换为带 TTL 租约探针(C1 契约);GovDoc 无探针行为有意放弃(缺陷) |
| 3 | 熔断时钟逐调用传 now(VT/GovDoc) |
替换为构造注入 now()(等价可测,签名更稳) |
| 4 | 有效阈值 = max(threshold, concurrency*2)(.env 注释约定) | 保留,装配层自动算 |
| 5 | 遥测:VT 持久连接+Lock+close() / GovDoc 每写新建连接 | 采 VT 形态 + asyncio.to_thread 桥接;GovDoc 形态放弃 |
| 6 | 遥测初始化/写入失败降级不冒泡、INSERT OR IGNORE 幂等、表 llm_calls 15 列 |
全保留;列只增(source_name/usage_source/cost) |
| 7 | 缓存:sha256(model+messages[+salt])、TTL>0 校验、读写失败静默降级(VT redis_cache.py) |
语义保留;key 公式替换(必填 namespace + 多模态 part 先摘要)→ 迁移后旧缓存一次性冷启动(迁移文档已记);GovDoc 无 salt/无隔离放弃(缺陷) |
| 8 | 瞬时判定:VT 宽(TimeoutException+TransportError)/ GovDoc 窄 |
采宽集(GovDoc 窄集放弃——ConnectError 漏网即 bug) |
| 9 | 429 body insufficient_quota → SourceDead;Retry-After 仅秒数(CHS invokers.py:144-166) |
保留 |
| 10 | usage 缺失按 est_tokens 估算 + usage_source 标注(CHS invokers.py:241) |
保留;VT"缺失填 0"放弃 |
| 11 | thinking:VT/GovDoc 子串猜测+注入开启;CHS 配置驱动+注入关闭 | 替换为注册表 profile + enable_thinking 三态(§2.3/§7) |
| 12 | qwen <think> 剥离(llm.py:147-164)、reasoning_content 增量提取(三家同款) |
保留(前者 registry 声明,后者 transport 通用) |
| 13 | SSE:ping/空行跳过、usage 帧旁路、畸形 JSON → 瞬时、强制 include_usage |
保留(三家同源纯函数移植) |
| 14 | 缺 [DONE]:VT strict / CHS salvage / GovDoc 无检查 | E3 可配默认 strict;GovDoc 无检查放弃(缺陷) |
| 15 | 重试:退避公式 min(base*2^n, max)*uniform(0.5,1.5) 与 Retry-After 取大;每 attempt 新 call_id;致命 force_open 即抛 |
保留(D13);两家 jitter 系数差异按 VT 收敛 |
| 16 | CHS 循环:源冷却备忘、gate_rejections==len(sources) 判 circuit_open、ResultInvalid 记成功、finally 嵌套 settle→release、Cancel 时 release_probe(governance.py:107-266) |
全保留(RetryMW 蓝本) |
| 17 | 限流契约:预扣/结算/退款、release 幂等、mark_progress(CHS limiter + 契约测试 5 项) | 保留;M1 内存实现同契约 |
| 18 | 缓存命中独立 cache_call_id + 记遥测(VT llm.py 缓存分支) |
保留(cache_hit=True, latency_ms=0) |
| 19 | JSON 解析:VT 内 ≥5 套并存、失败语义不一 | 替换为 JsonRepairStrategy 单实现;业务归一化经 normalize 钩子外置(§5 细则 3) |
| 20 | 非流式路径:三家均无(硬编码 stream=True) | 新增 stream=False 快路径(单 JSON 响应,仅 total 超时) |
| 21 | evolve_llm = llm 隐式共享实例(VT main.py) |
放弃隐式;共享 = 显式注入同一状态后端(§2.4) |
| 22 | 看门狗活性:thinking 增量刷新计时、ttft=首个任意 token(CHS invokers.py:55-79) |
保留(§4.4 Transport 行;CHS 迁移 R1) |
M2 备忘(不入 M1):CHS 图片 magic bytes 探测 mime(invokers.py:116-124)随 VLM 多模态迁移移入库 transport。
10. 非功能维度(逐条)
| 维度 | 回答 |
|---|---|
| 并发与取消 | 同一 client 被 N 协程并发调用为标准形态:所有中间件无实例级可变请求状态(A1);内存 limiter 用 asyncio 原语,断言单事件循环。CancelledError:退避 sleep / 限流等待 / __anext__ 全可取消;取消路径 finally 依次 release_probe(若探针)→ permit.settle(0)+release()(TPM 退款,承 CHS governance.py finally 语义)→ 尽力遥测(error="cancelled",写失败静默)→ 重抛;库内零 except BaseException |
| 降级方向 | 缓存 get/set 失败、遥测写失败 → warning 静默;limiter/breaker 后端操作失败 → GovernanceBackendError 上抛不放行(M1 内存后端仅内部 bug 会触发,契约照立,M2 Redis 直接继承) |
| 幂等与重复 | settle/release/release_probe 幂等(契约测试);遥测 call_id 主键 OR IGNORE;缓存同 key 重写同值无害 |
| 持久化与原子性 | SQLite WAL + busy_timeout,单持久连接 + to_thread,崩溃最多丢当次记录;缓存写在阶梯通过后(部分写入 = Redis 单 SET,原子);内存后端无持久化(进程死 = 状态清零,单进程语义下正确) |
11. 测试策略
| 层 | 内容 |
|---|---|
| unit(纯函数) | 看门狗(fake 迭代器+注入时钟,三层超时逐个触发)、SSE 解析(用三项目录制的真实帧样本二次构造,含畸形帧/usage 帧/缺 DONE)、退避公式、缓存 key 公式(namespace 隔离、salt、多模态摘要稳定性)、注册表、json_repair 链、错误翻译表逐行 |
| 契约(参数化后端) | limiter:CHS 5 项 + settle 幂等 + 装配守卫;breaker:状态机全迁移 + 探针租约过期回收 + force_open;M1 跑 memory,M2 同套跑 Redis |
| integration | 远程 Redis 缓存(命名空间 pgw:test:{uuid},teardown 清理,断连模拟降级方向)、SQLite 并发写、RetryMW×限流×熔断组合(换源/冷却/circuit_open/取消穿透——asyncio.Task.cancel 注入到退避与流式中途) |
| e2e | 真实网关冒烟(流式/非流式/structured 三档),输出落 tests/outputs/;GovDoc 与 Video-Tree 双最小接入冒烟 |
每步先写失败测试再实现(测试结果门);行为变更的证据 = 本会话内 pytest 先红后绿输出。
12. 纸面兼容验证(调用点反推)
| 调用点 | 形态 | 兼容性 |
|---|---|---|
VT loop.py:336 |
chat(messages, session_id=, cache_salt=) |
✅ 逐字兼容 |
VT summarizer.py:236 |
chat(messages, session_id=, parent_call_id=) |
✅ |
VT evolve.py:566 |
chat(messages) 纯位置 |
✅ |
GovDoc loop.py:377 |
chat(messages, session_id=, parent_call_id=) |
✅;其 LLMProvider Protocol(protocols.py:15-25)为我方签名子集,结构性满足 |
CHS tracking.py:406-428 |
catch ProviderUnavailableError → exc.retry_after_s/scope/reason |
✅ GatewayUnavailableError 字段同名(M2 接入) |
CHS classifiers.py:12 偷 import _extract_json_object |
→ structured="json" 档 |
✅ 语义覆盖(不修复→修复是增强,失败仍抛异常) |
13. 对 ARCHITECTURE.md 的反哺修订(经人类批准后执行)
- §6.1 reason 枚举勘误(本文档 §3):scope 级 5 值 + per_source 6 值,替换现文不实的 7 值清单。
- §5.1 新增字段清单补
structured_data;§5.2chat()的structured参数类型定稿为type[BaseModel] | Literal["json"] | None。 - §7.1 补 SSE 缺 [DONE] 的 per-source
missing_done语义(本文档 §6,承 CHS salvage 迁移约束)。 - §7.9 阶梯④补反馈模板与"重问叠加 NativeSchema"升级细则;归一化钩子外置声明。
- §8 模块图
middleware/清单补structured.py(StructuredMW);sources.py职责注明含选源策略(selector 不单设目录)。 - §4.3 默认层序更新为 遥测→缓存→结构化→重试循环(StructuredMW 插入位,本文档 §5)。
- §6.1
AllSourcesExhausted/CircuitOpenError的retry_after_s定稿为非可选 float(替换现文float | None;CHS 同款,0 表示"可立即重试")。