feat: judge whether reasoning actually happened from multiple signals
reasoning_tokens=None has been carrying two meanings at once, no reasoning and no report, and the library resolved the ambiguity by quietly claiming the first. ThinkingObservation splits them: UNKNOWN says the call left no signal, ABSENT says the provider reported zero. The verdict ranks evidence by hardness. Reasoning prose is the fact itself; reasoning_tokens is a report about the fact, so a missing report cannot overrule prose that is right there. The prose check strips first, since a gateway that returns whitespace is not evidence. The enum lives in types.py, not in the new thinking.py, because LLMResponse is typed on it and the innermost layer must not import a decision module.
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
"""推理这件事的全部**决策**: 请求侧注入形态、响应侧结果裁定、二者的对账。
|
||||
|
||||
与 `providers.py` 的分工: 那里是**注册表**(provider 长什么样,静态声明的存放
|
||||
与查找),这里是**决策**(拿声明和响应做判断)。P7"决策逻辑与状态存储分离"。
|
||||
|
||||
本模块**不定义** `ThinkingObservation` —— 它是 `LLMResponse` 的字段类型,归最
|
||||
内层 `types.py`;定义在这里会让 `types.py` 反向 import 决策模块(依赖铁律)。
|
||||
"""
|
||||
|
||||
from polygateway.types import ThinkingObservation
|
||||
|
||||
|
||||
def observe_thinking(*, thinking: str, reasoning_tokens: int | None) -> ThinkingObservation:
|
||||
"""由多信号裁定推理是否发生;判据按**证据硬度**排序(issue #16/#17)。
|
||||
|
||||
推理正文是事实本身,`reasoning_tokens` 是对事实的转述——转述缺失时事实仍然
|
||||
作数。2026-08-25 实测: MiniMax 这一路已不再返回
|
||||
`usage.completion_tokens_details`,而同一次调用里库拿得到 185 字符推理正文;
|
||||
只认 token 数的判据会把这种情形误判成"没推理"。
|
||||
|
||||
正文判据取 `strip()` 而非 truthy: 网关响应是外部输入,纯空白串不是证据(P5)。
|
||||
|
||||
判不出来时返回 `UNKNOWN` 而非 `ABSENT`——**不许把"没看见"说成"没发生"**。
|
||||
"""
|
||||
if thinking.strip():
|
||||
return ThinkingObservation.OBSERVED
|
||||
if reasoning_tokens is None:
|
||||
return ThinkingObservation.UNKNOWN
|
||||
return ThinkingObservation.OBSERVED if reasoning_tokens > 0 else ThinkingObservation.ABSENT
|
||||
@@ -10,6 +10,7 @@ import math
|
||||
import re
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
@@ -166,6 +167,27 @@ def canonical_sampling_json(merged: Mapping[str, Any]) -> str | None:
|
||||
return json.dumps(dict(merged), sort_keys=True, ensure_ascii=False)
|
||||
|
||||
|
||||
class ThinkingObservation(StrEnum):
|
||||
"""一次调用中"推理是否真的发生"的裁定结果(issue #16/#17)。
|
||||
|
||||
三态**不可折叠为布尔**: `UNKNOWN` 是"本次无任何信号,判不出来",与
|
||||
`ABSENT`("上游明确上报了未推理")语义不同。把前者折叠进后者,正是
|
||||
`reasoning_tokens=None` 制造的那个歧义——库据此静默宣称"没推理",而实际
|
||||
可能推理了且已计费(MiniMax-M3 非流式实测: completion 53 vs 关闭档 3,
|
||||
推理正文与 usage 明细双双不回传)。
|
||||
|
||||
裁定由 `thinking.observe_thinking` 做,本类只是取值域。**枚举定义在最内层
|
||||
而非决策层**: 它是 `LLMResponse` 的字段类型,放进 `thinking.py` 会让
|
||||
`types.py` 反向 import 决策模块(P7 依赖铁律)。
|
||||
|
||||
取值进遥测落库,改名即造成历史数据断层。
|
||||
"""
|
||||
|
||||
OBSERVED = "observed"
|
||||
ABSENT = "absent"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LLMResponse:
|
||||
"""一次治理调用的统一响应(与三项目超集兼容,ARCH §5.1)。"""
|
||||
|
||||
Reference in New Issue
Block a user