fix(llm): call_id 移入重试循环,每 attempt 独立

消除重试时遥测主键冲突的根因。每次 attempt 独立记录,
parent_call_id 不受影响(循环外固定),更利于事后诊断重试轨迹。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 00:12:38 -04:00
parent 5a91f392f0
commit f733c13dd1
2 changed files with 8 additions and 9 deletions
+6 -7
View File
@@ -295,12 +295,10 @@ class GovernedLLMClient:
if self._breaker.is_open(self._provider, time.monotonic()): if self._breaker.is_open(self._provider, time.monotonic()):
raise CircuitOpenError(f"熔断器已开启,拒绝调用 provider={self._provider}") raise CircuitOpenError(f"熔断器已开启,拒绝调用 provider={self._provider}")
# ② call_id 生成 # ② 缓存查询(cache 为 None 时跳过)— call_id 在缓存路径独立生成
call_id = str(uuid4())
# ③ 缓存查询(cache 为 None 时跳过)
cached = await self._cache.get(self._model, messages) if self._cache is not None else None cached = await self._cache.get(self._model, messages) if self._cache is not None else None
if cached is not None: if cached is not None:
cache_call_id = str(uuid4())
response = LLMResponse( response = LLMResponse(
content=cached.content, content=cached.content,
thinking=cached.thinking, thinking=cached.thinking,
@@ -312,10 +310,10 @@ class GovernedLLMClient:
ttft_ms=None, ttft_ms=None,
max_inter_token_ms=None, max_inter_token_ms=None,
cache_hit=True, cache_hit=True,
call_id=call_id, call_id=cache_call_id,
) )
await self._telemetry.record_llm_call( await self._telemetry.record_llm_call(
call_id=call_id, call_id=cache_call_id,
parent_call_id=parent_call_id, parent_call_id=parent_call_id,
session_id=session_id, session_id=session_id,
model_name=self._model, model_name=self._model,
@@ -333,9 +331,10 @@ class GovernedLLMClient:
) )
return response return response
# 重试循环 + 流式消费 # 重试循环 + 流式消费(每次 attempt 独立 call_id
last_exc: Exception | None = None last_exc: Exception | None = None
for attempt in range(self._max_retries): for attempt in range(self._max_retries):
call_id = str(uuid4())
attempt_start = time.monotonic() attempt_start = time.monotonic()
try: try:
content, thinking_text, ttft_ms, max_itoken_ms, usage = await self._call_streaming( content, thinking_text, ttft_ms, max_itoken_ms, usage = await self._call_streaming(
+2 -2
View File
@@ -197,9 +197,9 @@ async def test_transient_error_retries_and_records_telemetry():
success_calls = [c for c in telemetry.calls if c.get("error") is None] success_calls = [c for c in telemetry.calls if c.get("error") is None]
assert len(success_calls) == 1 assert len(success_calls) == 1
# 所有遥测记录应使用同一个 call_id(Important 2 修复验证 # 每次 attempt 应使用独立的 call_id(根因修复:防遥测主键冲突
call_ids = {c["call_id"] for c in telemetry.calls} call_ids = {c["call_id"] for c in telemetry.calls}
assert len(call_ids) == 1 assert len(call_ids) == 3 # 2 次失败 + 1 次成功 = 3 个独立 call_id
@pytest.mark.asyncio @pytest.mark.asyncio