Skip to content

fix(ai): ai_request_logs 에 세션 귀속 — 비용을 면접 단위로 볼 수 있게 - #264

Merged
i3months merged 1 commit into
devfrom
fix/ai-log-session-attribution
Sep 28, 2026
Merged

i3months merged 1 commit into
devfrom
fix/ai-log-session-attribution

Conversation

@i3months

Copy link
Copy Markdown
Member

테스트 면접(세션 102)을 끝까지 돌려 확인하다 나왔다.

AI 호출 39건, session_id 전부 NULL

면접 한 번에 39건의 AI 호출이 기록됐는데 전부 세션을 모른 채 기록됐다. 회귀가 아니라
오래된 구멍이다 — request_type 별 채워진 비율(전체 기간):

request_type 채워짐 / 전체
stt.transcribe 7 / 7 ← 유일
generate.feedback.panel 0 / 45
generate.feedback.synthesis 0 / 15
generate.feedback.coaching 0 / 13
generate.feedback.self_intro 0 / 13
tts.synthesize 0 / 17
embedding.embed 0 / 12
generate.followup.stream 0 / 9
generate.questions 0 / 8
analyze.document 0 / 5 ← 세션 없음(정상)

record_ai_call 과 CoreAiLogCallback 은 둘 다 session_id 를 받을 수 있게 만들어져
있었다.
넘기는 호출부가 voice consumer 하나뿐이었을 뿐이다. 오늘 반복해서 나온 패턴과
같다 — 배선은 있는데 아무도 안 쓴다.

그래서 "이 면접이 얼마나 들었나", "어느 세션이 게이트웨이 크레딧을 태웠나" 를 답할 수
없었다. 2026-09-17 에 크레딧이 소진돼 운영 AI 가 402 로 죽었을 때 정확히 이 질문을 못 했다.

방법 — traceId 와 같은 방식

체인 빌더마다 인자로 넘기면 LangChain 콜백 생성 지점 6곳과 체인 팩토리 시그니처가 전부
오염된다. 관측값은 횡단 관심사라 컨텍스트에 싣는다:

trace_context(envelope.trace_id,
              session_id=getattr(envelope.payload, "session_id", None),
              user_id=getattr(envelope.payload, "user_id", None))
  • 컨슈머 진입점이 payload 에서 꺼내 묶는다 (getattr — 문서 분석 payload 에는 session_id 가 없다)
  • record_ai_call 과 CoreAiLogCallback 이 명시 인자가 없을 때 거기서 읽는다
  • 명시 인자가 있으면 그쪽이 이긴다 (voice consumer 가 그렇게 쓴다)
  • 세션이 없는 흐름은 그대로 NULL — 억지로 채우지 않는다
  • session_id 는 로그 줄에도 실린다 — 로그만 봐도 어느 면접의 호출인지 보인다

테스트 — "넘겼다"가 아니라 "기록부에 도착한다"

가짜 CoreClient 로 실제 record_ai_log 호출 인자를 확인한다:

테스트 왜
record_ai_call 이 컨텍스트에서 집는다 이게 핵심 경로
명시 인자가 컨텍스트를 이긴다 voice consumer 의 기존 동작을 깨지 않는다
세션 없는 흐름은 NULL 로 기록되고 죽지 않는다 문서 분석이 여기 해당
LangChain 콜백도 컨텍스트에서 집는다 체인 빌더는 세션을 모른다
session_id 가 로그 줄에도 실린다

AI 전체 427건 통과, black·flake8 clean.

배포 후 확인할 것

다음 면접에서:

SELECT request_type, count(*), sum(input_tokens), sum(output_tokens)
  FROM ai_request_logs WHERE session_id = <다음 세션> GROUP BY 1;

지금은 이 쿼리가 0행이다.

테스트 면접(세션 102)을 끝까지 돌려 확인하다 나왔다. AI 호출 39건이 기록됐는데
session_id 가 **전부 NULL** 이었다.

회귀가 아니라 오래된 구멍이다. request_type 별 채워진 비율(전체 기간):

  stt.transcribe                 7 / 7      ← 유일
  generate.feedback.panel        0 / 45
  generate.feedback.synthesis    0 / 15
  generate.feedback.coaching     0 / 13
  generate.feedback.self_intro   0 / 13
  embedding.embed                0 / 12
  tts.synthesize                 0 / 17
  generate.questions             0 / 8
  generate.followup.stream       0 / 9
  analyze.document               0 / 5      ← 세션 없음(정상)

record_ai_call 과 CoreAiLogCallback 은 둘 다 session_id 를 **받을 수 있게**
만들어져 있었다. 넘기는 호출부가 voice consumer 하나뿐이었을 뿐이다.

그래서 "이 면접이 얼마나 들었나", "어느 세션이 게이트웨이 크레딧을 태웠나" 를
답할 수 없었다 — 2026-09-17 에 크레딧이 소진돼 운영 AI 가 402 로 죽었을 때
정확히 이 질문을 못 했다.

## 방법

체인 빌더마다 인자로 넘기면 LangChain 콜백 생성 지점 6곳과 체인 팩토리
시그니처가 전부 오염된다. 관측값은 횡단 관심사이므로 traceId 와 같은 방식으로
컨텍스트에 싣는다.

  trace_context(trace_id, session_id=..., user_id=...)

컨슈머 진입점이 payload 에서 꺼내 묶고(getattr 로 — 문서 분석 payload 에는
session_id 가 없다), record_ai_call 과 CoreAiLogCallback 이 명시 인자가 없을 때
거기서 읽는다. 명시 인자가 있으면 그쪽이 이긴다(voice consumer 가 그렇게 쓴다).

session_id 는 로그 줄에도 실린다 — 로그만 봐도 어느 면접의 호출인지 보인다.

## 테스트

여기서도 "넘겼다"가 아니라 **"기록부에 도착한다"** 를 본다. 가짜 CoreClient 로
실제 record_ai_log 호출 인자를 확인한다:

  record_ai_call 이 컨텍스트에서 session_id/user_id 를 집는다
  명시 인자가 컨텍스트를 이긴다
  세션 없는 흐름은 NULL 로 기록되고 죽지 않는다
  LangChain 콜백도 컨텍스트에서 집는다 (체인 빌더는 세션을 모른다)
  session_id 가 로그 줄에도 실린다

AI 전체 427건 통과.
@i3months
i3months merged commit 0b896bc into dev Sep 28, 2026
6 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