feat(interviews): LLM stub mode for deterministic CI tests

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Moellmann 2026-05-23 12:08:29 +02:00
parent 29be754ff4
commit eb3c3629c1
2 changed files with 54 additions and 4 deletions

View File

@ -32,6 +32,31 @@ class LLMClient:
base_url=self.base_url base_url=self.base_url
) )
def _stub_key(self, messages: list[dict]) -> str:
user_msg = next((m["content"] for m in reversed(messages) if m.get("role") == "user"), "")
sys_msg = next((m["content"] for m in messages if m.get("role") == "system"), "")
# Allow callers to embed an explicit stub_key=... token
for chunk in user_msg.split():
if chunk.startswith("stub_key="):
return chunk[len("stub_key="):]
import hashlib
return hashlib.sha256((sys_msg + "|" + user_msg).encode("utf-8")).hexdigest()[:12]
def _stub_response(self, messages: list[dict]) -> str:
import json as _json
return _json.dumps(self._stub_response_json(messages), ensure_ascii=False)
def _stub_response_json(self, messages: list[dict]) -> dict:
key = self._stub_key(messages)
# Deterministic centered Likert + plausible open text
digit = sum(ord(c) for c in key) % 5 + 1
return {
"stub_key": key,
"responses": {"item_001": digit, "item_002": digit, "item_003": (digit % 5) + 1},
"confidence": {"item_001": 0.7, "item_002": 0.7, "item_003": 0.6},
"open_comment": f"stub:{key}",
}
def chat( def chat(
self, self,
messages: List[Dict[str, str]], messages: List[Dict[str, str]],
@ -41,16 +66,20 @@ class LLMClient:
) -> str: ) -> str:
""" """
发送聊天请求 发送聊天请求
Args: Args:
messages: 消息列表 messages: 消息列表
temperature: 温度参数 temperature: 温度参数
max_tokens: 最大token数 max_tokens: 最大token数
response_format: 响应格式如JSON模式 response_format: 响应格式如JSON模式
Returns: Returns:
模型响应文本 模型响应文本
""" """
from app.config import Config
if getattr(Config, "LLM_STUB_MODE", False):
return self._stub_response(messages)
kwargs = { kwargs = {
"model": self.model, "model": self.model,
"messages": messages, "messages": messages,
@ -75,15 +104,19 @@ class LLMClient:
) -> Dict[str, Any]: ) -> Dict[str, Any]:
""" """
发送聊天请求并返回JSON 发送聊天请求并返回JSON
Args: Args:
messages: 消息列表 messages: 消息列表
temperature: 温度参数 temperature: 温度参数
max_tokens: 最大token数 max_tokens: 最大token数
Returns: Returns:
解析后的JSON对象 解析后的JSON对象
""" """
from app.config import Config
if getattr(Config, "LLM_STUB_MODE", False):
return self._stub_response_json(messages)
response = self.chat( response = self.chat(
messages=messages, messages=messages,
temperature=temperature, temperature=temperature,

View File

@ -0,0 +1,17 @@
import json
from app.utils.llm_client import LLMClient
def test_stub_mode_returns_deterministic_canned_json(monkeypatch):
monkeypatch.setenv("LLM_STUB_MODE", "true")
from app.config import Config
Config.LLM_STUB_MODE = True
client = LLMClient(api_key="x", base_url="x", model="x")
messages = [
{"role": "system", "content": "You are persona_42. Return JSON."},
{"role": "user", "content": "stub_key=longitudinal:item_001"},
]
out1 = client.chat_json(messages=messages, temperature=0.0)
out2 = client.chat_json(messages=messages, temperature=0.0)
assert out1 == out2
assert isinstance(out1, dict)