Skip to content

feat(ai): 체인 밖 외부 호출(TTS·임베딩·라이브 STT) 호출 로깅 - #247

Merged
i3months merged 1 commit into
devfrom
feat/external-call-logging
Sep 25, 2026
Merged

i3months merged 1 commit into
devfrom
feat/external-call-logging

Conversation

@i3months

Copy link
Copy Markdown
Member

작업 내용

  • observability/ai_call_log.py — 공용 record_ai_call(fire-and-forget) + measure_ai_call(지연 측정 + 성공/실패 자동 기록)
  • TTS → LoggingTtsProvider 데코레이터 (tts.synthesize)
  • 임베딩 → 시도마다 한 행 + [n/N] 접두 (embedding.embed)
  • 라이브 STT → 스트림 1건당 한 행 (stt.live)
  • voice_consumer 의 수동 기록을 공용 헬퍼로 통일 (동작 동일)
  • MessagingRuntime.core_client 노출 → WS 핸들러도 같은 클라이언트 사용

변경 이유 / 배경

CoreAiLogCallback 은 LangChain 콜백이라 체인을 타는 호출만 잡습니다. 확인해 보니 record_ai_log 호출부가 이게 전부였습니다:

chain/{document_analysis,feedback_generation,followup_generation,question_generation,pdf_vision}.py
messaging/consumers/voice_consumer.py   ← 배치 STT 만 수동으로

즉 TTS·임베딩·라이브 STT 는 ai_request_logs 에 한 줄도 남지 않았습니다.

그 대가를 운영에서 치렀습니다. TTS 요청 245건 중 43건이 실패했는데(전부 6월) 원인을 사후에 알 방법이 없습니다. 지금 "정상"으로 보이는 것도 정상이 아니라 안 보이는 것일 수 있습니다.

반대 사례가 이 PR 의 근거입니다 — 직전 PR 들(#244/#246)에서 STT 와 LLM 타임아웃을 근거를 갖고 고칠 수 있었던 건 순전히 이 표에 latency 와 status 가 남아 있었기 때문입니다:

관측 가능했던 것 결과
STT 성공 p50 3.6s vs 실패 60.8s 타임아웃 원인 특정, 재시도 도입
LLM Pro 성공 최대 56s(=타임아웃 30s + 재시도 26s) 30s 가 짧다는 걸 증명, 60s 로
TTS 없음 43건 실패 원인 영구 미상

설계 결정

TTS 는 consumer 가 아니라 provider 를 감쌌습니다. TTS 는 generate.tts consumer 말고도 followup consumer 의 문장 단위 인라인 합성(Part B)에서 호출됩니다. consumer 쪽만 계측하면 라이브 경로가 통째로 빠집니다. 대신 세션/사용자 맥락은 싣지 않습니다 — synthesize(text, *, voice) 시그니처를 호출부마다 바꿔야 해서 비용 대비 이득이 적고, 실패율·지연·모델별 사용량은 이것만으로 보입니다.

재시도가 있는 경로는 시도마다 한 행입니다(STT 규약과 동일). 실패율의 분모가 '요청'이 아니라 '시도'가 되므로, 사용자에게 보인 실패만 세려면 [N/N] 으로 끝난 행만 셉니다. 이 규약 덕분에 재시도율 자체가 관측됩니다 — 임베딩은 429 재시도로 사용자에게 실패가 안 보였지만 몇 번 만에 통과하는지 아무도 몰랐고, batch_size·백오프를 조정하려면 그 숫자가 필요합니다.

라이브 STT 의 빈 전사는 FAILED 로 남깁니다. 무음일 수도, 상류가 조용히 죽었을 수도 있습니다 — 지금은 구분할 단서조차 없어서 일단 구분 가능한 상태로 만듭니다.

관측이 본 작업을 죽이면 안 됩니다. 기록은 태스크로 떼고 예외는 삼킵니다(Core 가 죽어도 TTS 합성은 계속). 회귀 테스트로 고정했습니다.

테스트

  • tests/test_ai_call_log.py 7건 — fire-and-forget, Core 장애 무영향, 500자 절단, 지연 측정, 실패 기록 + 재raise, TTS 데코레이터 성공/실패
  • tests/test_embedder.py +1건 — 429 → 재시도 → 성공 시 [1/2] FAILED + SUCCESS 두 행
  • uv run pytest 413 passed, black/flake8 clean

영향 범위

  • DB 마이그레이션: 없음 (ai_request_logs 스키마 그대로)
  • API contract 변경: 없음
  • 환경변수: 없음
  • 기록량 증가: TTS 는 문장 단위 합성이라 세션당 행이 꽤 늘어납니다. ai_request_logs 에는 보존 정책이 있어(VolatileRetention) 무한 증가하진 않지만, 보존 기간·주기가 적절한지 봐주세요.

리뷰어 체크포인트

  • TTS 기록량 — 문장 단위 합성이면 질문 하나에 여러 행입니다. 세그먼트를 묶어 한 행으로 낼지, 지금처럼 호출 단위로 둘지 판단 부탁드립니다.
  • 라이브 STT 의 빈 전사를 FAILED 로 분류한 것 — 무음이 정상인 경우도 있어 실패율을 부풀릴 수 있습니다.
  • docs/observability.md §3.1 에 두 기록 경로 표를 추가했습니다. 새 외부 API 를 붙일 때 이 표를 갱신하는 규약이 지켜질 만한지.

🤖 Generated with Claude Code

`CoreAiLogCallback` 은 LangChain 콜백이라 체인을 타는 호출만 잡는다. TTS·임베딩·
라이브 STT 는 `ai_request_logs` 에 한 줄도 남지 않았다.

그 대가를 운영에서 치렀다 — TTS 요청 245건 중 43건이 실패했는데 원인을 사후에
알 방법이 없다. 지금 "정상"으로 보이는 것도 정상이 아니라 안 보이는 것이다.
반대로 STT·LLM 타임아웃을 근거 있게 고칠 수 있었던 건 순전히 이 표에 latency 와
status 가 남아 있었기 때문이다(성공 p50 3.6초 vs 실패 60.8초).

- observability/ai_call_log.py — 공용 record_ai_call(fire-and-forget) +
  measure_ai_call(지연 측정 + 성공/실패 자동 기록). 관측이 본 작업을 죽이지
  않도록 태스크로 떼고 예외는 삼킨다.
- TTS: LoggingTtsProvider 데코레이터. consumer 가 아니라 provider 를 감쌌다 —
  TTS 는 generate.tts consumer 말고도 followup 의 문장 단위 인라인 합성에서도
  호출되므로 consumer 만 계측하면 라이브 경로가 통째로 빠진다.
- 임베딩: 시도마다 한 행 + `[n/N]` 접두. 429 재시도로 사용자에겐 실패가 안
  보였지만 몇 번 만에 통과하는지 아무도 몰랐다.
- 라이브 STT: 스트림 1건당 한 행. 빈 전사는 FAILED 로 남겨 무음과 상류 무응답을
  구분할 단서를 만든다.
- voice_consumer 의 수동 기록도 공용 헬퍼로 통일(동작 동일).
- MessagingRuntime.core_client 노출 → WS 핸들러도 같은 클라이언트 사용.
@i3months
i3months merged commit 5c3add4 into dev Sep 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant