From 11131885a6196a63ae6e3c5df80406d9a3901ff3 Mon Sep 17 00:00:00 2001 From: Eric Wang Date: Thu, 22 Oct 2026 23:19:11 +0800 Subject: [PATCH] feat(api): warn on unrecognized stage codes (B4 gray-release monitoring) LLM/output stage codes outside STATUS_CODE_MAP were passed through silently with an empty nextStage - undetectable drift between the prompt output code set and the endpoint mapping table. - endpoints: _stage_label / _warn_unknown_stage_code helpers; both streaming and non-streaming paths emit a warning with sessionId, clientMode, raw and normalized codes when the label lookup misses (transparency behavior unchanged: code passes through, label empty) - known codes and covered internal normalizations (e.g. 2020->0002) stay silent; the warning fires on the normalized value Verified: 229 unit tests (4 new monitoring tests) --- src/api/endpoints.py | 40 ++++++++++- test/api/test_unknown_stage_code.py | 107 ++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 test/api/test_unknown_stage_code.py diff --git a/src/api/endpoints.py b/src/api/endpoints.py index 3c5bf25..13907b4 100644 --- a/src/api/endpoints.py +++ b/src/api/endpoints.py @@ -49,6 +49,28 @@ STATUS_CODE_MAP = { '2016': '确认双车中的车牌' } + +def _stage_label(normalized_stage_code: str) -> str: + """阶段码的中文标签;未知码返回空串(B4 监测点)。""" + return STATUS_CODE_MAP.get(normalized_stage_code, '') + + +def _warn_unknown_stage_code( + *, session_id: str, client_mode: str, + raw_stage_code: str, normalized_stage_code: str, +) -> None: + """B4:LLM/后端输出映射表外的阶段码时告警(灰度监测)。 + + 未知码当前行为:原样透传 nextStageCode、nextStage 为空串。 + 告警带全定位信息,用于确认提示词输出码集是否收敛。 + """ + logger.warning( + "Unrecognized stage code " + f"sessionId={session_id} clientMode={client_mode} " + f"rawStageCode={raw_stage_code} " + f"normalizedStageCode={normalized_stage_code}" + ) + def normalize_stage_code(stage_code: str) -> str: """Normalize FastGPT stage codes to external API stage codes.""" if stage_code in ['3001', '3002', '1002']: @@ -315,7 +337,14 @@ async def chat( client_mode, normalized_stage_code, ) - nextStage = STATUS_CODE_MAP.get(normalized_stage_code, '') + nextStage = _stage_label(normalized_stage_code) + if not nextStage: + _warn_unknown_stage_code( + session_id=json_data['sessionId'], + client_mode=client_mode, + raw_stage_code=state_code, + normalized_stage_code=normalized_stage_code, + ) logger.info( "FastGPT stream stage_code parsed " f"sessionId={json_data['sessionId']} " @@ -470,7 +499,14 @@ async def chat( raise ValueError("大模型回复中缺少state") normalized_stage_code = normalize_stage_code(raw_stage_code) nextStageCode = external_stage_code(client_mode, normalized_stage_code) - nextStage = STATUS_CODE_MAP.get(normalized_stage_code, '') + nextStage = _stage_label(normalized_stage_code) + if not nextStage: + _warn_unknown_stage_code( + session_id=json_data['sessionId'], + client_mode=client_mode, + raw_stage_code=raw_stage_code, + normalized_stage_code=normalized_stage_code, + ) form_update = result.form_update logger.info( "Chat non-stream completed " diff --git a/test/api/test_unknown_stage_code.py b/test/api/test_unknown_stage_code.py new file mode 100644 index 0000000..480986f --- /dev/null +++ b/test/api/test_unknown_stage_code.py @@ -0,0 +1,107 @@ +"""B4 监测:未识别 stage 码告警(灰度期确认提示词输出码集收敛)。 + +LLM/后端输出映射表(STATUS_CODE_MAP)外的阶段码时,当前行为是 +原样透传 nextStageCode、nextStage 为空串——静默异常。 +本测试固定:两条路径(非流式/流式)都会打 warning,已知码不打。 +""" + +import pytest + +import src.api.endpoints as endpoints +from src.api.endpoints import chat +from src.backends.chat import ChatInput, ChatResult, TextDelta +from src.schemas.models import ProcessRequest_chat + + +class FakeBackend: + """固定返回指定 state 前缀的 backend。""" + + def __init__(self, content: str): + self._content = content + + async def complete(self, chat_input: ChatInput): + return ChatResult(content=self._content, status_code=None, form_update={}) + + async def stream(self, chat_input: ChatInput): + yield TextDelta(self._content) + + +def make_request(): + return ProcessRequest_chat( + sessionId="session-b4", + timeStamp="20261022120000", + text="测试输入", + ) + + +class _WarningRecorder: + def __init__(self): + self.messages: list[str] = [] + + def warning(self, message: str) -> None: + self.messages.append(message) + + def __getattr__(self, _name): + # info/debug/error 等:no-op(本测试只关心 warning) + return lambda *a, **k: None + + +@pytest.mark.asyncio +async def test_unknown_stage_code_warns_non_stream(monkeypatch): + recorder = _WarningRecorder() + monkeypatch.setattr(endpoints, "logger", recorder) + backend = FakeBackend("9999收到了") + + response = await chat(make_request(), stream=False, backend=backend) + + assert response.nextStageCode == "9999" + assert response.nextStage == "" + assert len(recorder.messages) == 1 + assert "rawStageCode=9999" in recorder.messages[0] + assert "sessionId=session-b4" in recorder.messages[0] + + +@pytest.mark.asyncio +async def test_unknown_stage_code_warns_stream(monkeypatch): + recorder = _WarningRecorder() + monkeypatch.setattr(endpoints, "logger", recorder) + backend = FakeBackend("9999收到了") + + response = await chat(make_request(), stream=True, backend=backend) + chunks = [] + async for chunk in response.body_iterator: + chunks.append(chunk.decode() if isinstance(chunk, bytes) else chunk) + + assert '"nextStageCode": "9999"' in "".join(chunks) + assert len(recorder.messages) == 1 + assert "rawStageCode=9999" in recorder.messages[0] + + +@pytest.mark.asyncio +async def test_normalized_internal_code_warns_with_normalized_value(monkeypatch): + """归一化映射表内的内部码(如 2020->0002 已是已知码)不告警; + 映射表未覆盖的内部码按归一化后的值告警。""" + recorder = _WarningRecorder() + monkeypatch.setattr(endpoints, "logger", recorder) + known = FakeBackend("2020识别不了") + response = await chat(make_request(), stream=False, backend=known) + assert response.nextStageCode == "0002" # 归一化生效 + assert recorder.messages == [] # 已知码不告警 + + unknown = FakeBackend("3009x") + response = await chat(make_request(), stream=False, backend=unknown) + assert response.nextStageCode == "3009" + assert len(recorder.messages) == 1 + assert "normalizedStageCode=3009" in recorder.messages[0] + + +@pytest.mark.asyncio +async def test_known_stage_code_no_warning(monkeypatch): + recorder = _WarningRecorder() + monkeypatch.setattr(endpoints, "logger", recorder) + backend = FakeBackend("1002继续") + + response = await chat(make_request(), stream=False, backend=backend) + + assert response.nextStage == "通话中" + assert recorder.messages == []