- ⑥ be_v2↔chat_server 경우의 수 대조 결과(처리 방향 표) - ⑦ agent 가 bot_chat_type/indicator_value 를 직접 내려주도록 요청(backend passthrough 준비 완료) - ⑧ backend 수정으로 생긴 agent 계약 의존성(배송 input_mode 유지, anchor_price 재계산 금지, mock 부재 등) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
15 KiB
Chat 오류 수정 — agent 측 핸드오프
작성: backend/frontend 담당. 대상: agent(9500) 담당자. 협상 진행 중 발생하던 오류(대화 막힘·오진행·desync)를 분석해 ①~⑤로 정리했다. backend/frontend 에서 고칠 수 있는 부분은 이미 반영했고(아래 "backend/front 완료"), agent 코드 수정이 필요한 항목만 이 문서로 넘긴다. 관련 계약은 AGENT_INTEGRATION.md 도 함께 참고.
배경 — 무엇이 문제였나
backend 와 agent 가 세션 상태를 각자 독립적으로 관리한다(backend=negotiation.chats, agent=learning.chat_sessions).
그런데 backend 는 매 턴 session_id 만 보내고 step 은 보내지 않아, 한쪽 상태가 어긋나면(리셋/타임아웃/재시도)
영구 desync 가 발생한다(예: backend 는 오프닝인 줄 아는데 agent 는 가격협상 단계부터 재개).
선행 시스템(KT be_v2 → chat_server)은 매 턴 step+index 를 보내 클라이언트가 상태를 쥐는 방식이라 이 문제가 없었다.
엔진 자체는 정상이다 — 양쪽 상태가 맞고 입력 모드가 맞으면 흐름(서비스안내→담당자확인→협상품목안내→기존가격제시→가격협상_확인→협상완료→협상종료)도 합의가 기록도 정상.
① 세션 step desync — agent 가 client_step 을 검증/동기화해야 함
backend/front 완료
- backend 가 매 턴 요청 body 에
client_step(backend 가 보는 직전 봇 step)을 함께 보낸다. (backend/services/agent_client.pyHttpAgentClient body,chat_service._agent_context) - frontend 는 desync 의심 에러(1404/1405) 시
messages를 다시 불러와 화면을 서버 기준으로 리싱크한다.
agent 가 해줘야 할 일 ⚠️
POST /v1/chat요청에 새로 추가된client_step(Optional[str]) 을 받는다.- agent 세션(
learning.chat_sessions.step)과client_step이 다르면 desync 다. 다음 중 하나로 대응:- (권장) agent 가 자기 step 을 정답으로 보고 응답하되, 응답에 현재
step/client_step을 정확히 실어 보내 backend/front 가 따라오게 한다. (이미Res_Chat.step있음 — 항상 신뢰 가능한 값으로 채울 것) - 또는 desync 가 크면 명시적 에러/리셋 신호를 주어 backend 가 해당 세션을 재동기화하게 한다.
- (권장) agent 가 자기 step 을 정답으로 보고 응답하되, 응답에 현재
- (대안) 세션 상태 조회 API 를 제공하면 backend 가 타임아웃/재진입 시 정합을 맞출 수 있다:
GET /v1/chat/sessions/{session_id}→{ step, ended, ... }.
② agent 타임아웃 시 재시도 desync — /chat 멱등성 필요
backend/front 완료
- 타임아웃을 일반 실패와 구분한다(에러코드
CHAT_AGENT_TIMEOUT=1405). backend 는 타임아웃 시 선점 유저 메시지를 롤백하고 경고 로깅한다(chat_service.send). - frontend 는 1405 시 메시지를 리싱크한다.
문제의 본질: 타임아웃은 "agent 가 이미 턴을 처리(step 전진)했는데 응답만 늦은" 경우일 수 있다. 이때 backend 가 롤백하면 backend 는 "안 일어난 일", agent 는 "전진" → desync. 단순 재시도하면 중복 전진.
agent 가 해줘야 할 일 ⚠️
POST /v1/chat을 멱등(idempotent) 하게 만든다. 같은session_id+ 같은 턴(예: backend 가 보낼client_step또는 멱등키)에 대한 재요청은 이미 처리한 결과를 그대로 반환하고 step 을 다시 전진시키지 않는다.- 멱등키 후보:
(session_id, client_step, user_input)또는 backend 가 헤더로 보낼Idempotency-Key(요청 시 합의 필요).
- 멱등키 후보:
- 멱등이 어렵다면 ①-3 의 세션 상태 조회 API 만이라도 제공. backend 가 타임아웃 후 현재 step 을 읽어 재시도 여부를 판단한다.
backend 의 agent 타임아웃 한계는
config.local.toml [AgentConfig] timeout_sec(현재 10초). 필요 시 조정 가능.
③ 입력-모드 검증 — ✅ backend/front 에서 완결 (agent 작업 없음)
- backend 가 직전 봇 메시지의
input_mode와 이번 유저 입력을 대조해, 어긋나면 agent 로 넘기지 않고CHAT_INPUT_MODE_MISMATCH=1404반환. (chat_service._input_matches_mode) — price 단계에 버튼텍스트, yes_no 단계에 가격 같은 케이스를 막아 "제자리걸음/오진행"을 차단. - frontend 는 1404 시 리싱크.
- 단, agent 응답의
input_mode/input_options가 정확해야 이 검증이 옳게 동작한다. agent 는 각 step 의input_mode(confirm·yes_no·percent·price·delivery_type)와input_options(버튼 라벨)를 정확히 채워 보낼 것.
④ agent 로 가는 협상 컨텍스트 부족 + RL 미작동 ⚠️ (가장 중요)
확인된 현상: 정상 완료된 협상에서도 learning.experience_logs=0, chat_sessions.used_action_ids=[].
즉 Q-learning 카드선택(RL)이 한 번도 작동하지 않았다. 흐름이 기존가격제시 → 가격협상_확인 으로 바로 가며 RL 가격협상 단계를 건너뛴다.
backend/front 진행 상황 — agent state 입력 5개 차원의 데이터 소스 현황:
| 필드 | agent 차원 | DB 소스 | 상태 |
|---|---|---|---|
target_price |
price_zone | negotiation.sessions.target_price |
✅ 실데이터 |
anchor_price |
price_zone | quotation_settings.anchoring_value → round(target*(1-value)) |
✅ 실데이터 (배선 완료) |
partner_count |
partner | 견적당 negotiation.sessions 개수 |
✅ 실데이터 (배선 완료) |
revenue_amount |
revenue | ❌ 스키마에 컬럼 없음 | ⚠️ 기본값 20,000,000 — 논의 필요 |
distribution_code |
distribution | ❌ 컬럼 없음 (code_map 키여야 함) | ⚠️ 기본값 "A" — 논의 필요 |
acceptance_ratio |
acceptance | ❌ 컬럼 없음 | ⚠️ 기본값 0.05 — 논의 필요 |
→ anchor_price/partner_count 는 실 DB 값으로 배선했다(chat_service._agent_context). 조회 실패 시 각각 target*0.99 / 1 폴백(항상 양수 보장 → agent state ValueError 방지).
🔴 논의가 필요한 부분 — revenue_amount / distribution_code / acceptance_ratio
이 셋은 현재 우리 스키마(quotation, quotation_settings, partner.items, partner.suppliers 등)에 대응 컬럼이 전혀 없다.
일단 기본값으로 고정해 둔다(20,000,000 / "A" / 0.05). 단, 이 상태에서는 agent 가 모든 협상을 같은 state 로 보아
RL 이 상황을 구분하지 못하므로, 아래를 agent 담당자와 합의한 뒤 backend 스키마/시드/배선을 확정해야 한다:
- 각 필드의 정확한 의미·단위·출처 정의
revenue_amount(매출액): 누구의 매출인가(거래처/공급사/품목 단위?), 단위(원), 어느 시점 값인가.distribution_code(유통 코드): 우리가 어떤 분류로 채울지 + agentconfig code_map의 유효 키 목록(없는 코드면 agent 가ValueError→ RL skip). 코드맵 공유 필수.acceptance_ratio(가격 수용률 0~1): 산출 정의(과거 협상 이력 집계? 어느 기간/단위?) — 집계 로직 주체(backend/agent) 합의.
- 합의 후 backend 작업: 위 정의에 맞춰 스키마 컬럼 추가(예:
quotation_settings/ 신규 테이블) + 시드 +_agent_context배선. - (별개) RL 단계가 왜 안 타는지 점검: 재협상(1:1) 단일라운드 흐름에서
가격협상(카드선택) 단계가 실행되도록 step 라우팅 확인. 의도적으로 안 타는 거라면 그 조건(예: 재견적/멀티라운드에서만)을 backend 에 알려줄 것.
⑤ 합의가 기록 — ✅ backend/front 에서 완결 (agent 작업 없음)
- frontend 는 가격 입력 턴에
user_input_type:"price"를 정확히 보낸다(확인됨). - backend 는 종료(success) 시 입찰가를
이번 턴 가격 → 마지막 제시가 → 목표가순으로 확정하며, 협상 중 제시가가 하나도 기록되지 않아 목표가로 폴백하면 경고 로깅한다(chat_service.send). 운영 로그에서 이 경고가 보이면 가격 캡처 누락을 의심할 것.
⑥ be_v2↔chat_server 경우의 수 대조 결과 (선행 시스템 전수 비교)
선행 시스템(be_v2↔chat_server)의 채팅 전체 경우의 수를 우리 backend↔agent 가 소화하는지 대조했다.
핵심 종료 폼(summaryRSP/CM, rejectRSP/CM, 정보변경)은 정합하게 소화되며(우리 backend _resolve_bot_chat_type
= chat_server step별 type 매핑과 일치, be_v2 의 reject success=True 버그도 미승계), 아래만 후속 처리한다.
| 항목 | 처리 방향 |
|---|---|
| 재견적 1:N 교차집계(입찰종료/동가입찰/선호공급사/견적마감) | negodata 에서 추후 처리 (현 chat 흐름엔 세션 단위 확정만 있음) |
delivery_type 미영속 |
✅ backend 수정 완료 — 재견적 배송형태선택 값을 summaryCM 요약(delivery_type)에 담는다(chat_service._delivery_choice). |
indicator 협상 지표 |
⚠️ agent 작업 요청 (⑦) |
폼 타입(bot_chat_type) 직접 전달 |
⚠️ agent 작업 요청 (⑦) — backend passthrough 준비 완료 |
card_data(경쟁사 가격차 등 카드 표시 데이터) |
🔵 추후 공동 논의 |
mbti / is_new_quote |
✅ 우리 프로젝트에서 불필요 — 제외 확정 |
⑦ agent 가 표현 계약을 직접 책임지도록 (chat 단순화 P1)
배경/문제: 선행 chat_server 는 step JSON 의 type(summaryRSP/CM·rejectRSP/CM·indicator)을 엔진이 직접 응답에 실었다.
우리는 이 책임을 backend 로 옮겨, backend 가 agent 의 내부 step 문자열(협상완료/협상실패/결과안내/결과제출)을
하드코딩 집합과 대조해 폼을 역유도한다(chat_service._resolve_bot_chat_type). agent 가 step 이름을 하나만 바꿔도
backend 가 조용히 폼을 None 으로 떨구는 취약 결합이다. indicator 게이지도 agent 가 값을 안 보내 통째 비활성.
backend/front 완료 ✅
- backend 는 이제 agent 응답의
bot_chat_type/indicator_value를 있으면 그대로 신뢰하고, 없으면 기존 step 기반(_resolve_bot_chat_type)으로 폴백한다(agent_client.AgentTurn,chat_service.send). → agent 가 보내기 시작하면 backend 코드 변경 없이 자동 전환되고, step-이름 결합은 폴백으로만 남는다. - backend 는
indicator_value를negotiation.chats.indicator_value컬럼에 영속 +ChatMessage.indicator_value로 전달한다. - frontend 는 이미
bot_chat_type분기(요약/거부/지표)와 indicator 게이지 컴포넌트가 구현·연결돼 있어, agent 가 값만 채우면 즉시 표시된다.
agent 가 해줘야 할 일 ⚠️
Res_Chat에bot_chat_type: Optional[str]추가하고 각 step 에서 정확히 채울 것 (summaryRSP/summaryCM/rejectRSP/rejectCM/indicator, 일반 텍스트는 None/text).- 이러면 backend 가 step 이름을 추측하지 않으므로, agent 가 step 명을 바꿔도 폼이 안 깨진다.
Res_Chat에indicator_value: Optional[float](1~99) 추가하고가격협상(카드선택) 턴에 채울 것. ⚠️ backend 컬럼이NUMERIC(8,6)(절대값 <100)이라 반드시 1~99 범위(100 금지). (가능하면indicator_rangePZ1/2/3 도)- (선택) 종료 폼 단계(
협상완료등)는chat_end=False+bot_chat_type=summaryXXX로, 실제 종료는 다음협상종료턴chat_end=True로 — 현 흐름 유지면 OK.
⑧ backend 수정으로 생긴 agent 계약 의존성 (agent 가 깨지 말아야 할 것)
이번 backend 정리(use_mock 제거, anchor/배송 배선, 응답 passthrough)로 agent 응답에 대한 묵시적 의존성이 생겼다. agent 가 아래를 바꾸면 backend 기능이 조용히 깨진다 — 업데이트라기보단 "유지 필요" 항목이다.
-
배송형태선택단계는 응답input_mode를 정확히"delivery_type"으로 보낼 것.- backend 는 재견적 요약(summaryCM)의 배송형태(
delivery_type)를,input_mode=="delivery_type"인 봇 메시지 직후의 유저 선택 라벨로 캡처한다(chat_service._delivery_choice). - 이 step 의
input_mode를 다른 값으로 바꾸면 배송형태가 요약에 안 담긴다.input_options라벨(협력사배송/지정택배배송/픽업배송)도 유지 권장(프론트 표시·매핑 기준).
- backend 는 재견적 요약(summaryCM)의 배송형태(
-
anchor_price는 backend 가 계산해 보내므로 agent 는 받은 값을 그대로 쓸 것(재계산/덮어쓰기 금지).- backend 가 견적설정
quotation_settings.anchoring_value로anchor = round(target*(1-value))를 계산해 요청 body 에 넣는다. - agent 가 자체
anchor_for(target)로 다시 계산하면 backend 와 어긋난다. 요청의anchor_price를 신뢰할 것.
- backend 가 견적설정
-
backend 에 더 이상 mock 이 없다 → agent 가 반드시 떠 있어야 한다.
use_mock/MockAgentClient제거됨. agent 미기동/오류 시 chat 은CHAT_AGENT_UNAVAILABLE(1402)로 degrade(프론트 toast). 로컬/CI 에서도 실제 agent 연동 전제.
-
(재확인)
partner_count는 backend 가 '견적당 세션 수'로 산출해 보낸다 — agent 는 받은 값으로 partner 차원(single/multiple/none)만 판정.
변경된 POST /v1/chat 요청 body (backend → agent)
{
"session_id": "<negotiation.sessions.session_id>", // 그대로 세션 키로 사용 (핸드오프 #1, 기존)
"rq_type": "재협상 | 재견적",
"user_input": "<버튼텍스트 | 가격문자열 | null(오프닝)>",
"target_price": 100000,
"anchor_price": 99000, // ④ quotation_settings.anchoring_value 기반 (실데이터)
// ▼ RL state 입력 (④)
"revenue_amount": 20000000, // ④ 🔴 기본값 고정 — DB 소스 없음, 논의 필요
"distribution_code": "A", // ④ 🔴 기본값 고정 — DB 소스 없음, code_map 키여야 함, 논의 필요
"partner_count": 1, // ④ 견적당 세션 수 (실데이터)
"acceptance_ratio": 0.05, // ④ 🔴 기본값 고정 — DB 소스 없음, 논의 필요
"client_step": "기존가격제시" // ① backend 가 보는 직전 봇 step (desync 감지용)
}
헤더: X-Tenant-ID: <견적(갑) company_id> (기존)
요약: agent 작업 필요 = ①(step 동기화/응답 step 신뢰), ②(/chat 멱등 또는 세션상태 조회 API), ④(RL 단계 라우팅 + state 데이터 소스 합의), ⑦(bot_chat_type+indicator_value 응답 추가).
③⑤ 는 backend/front 에서 완결. ⑥ 의 delivery_type·⑦ 의 passthrough/게이지는 backend·front 준비 완료(agent 가 값만 채우면 동작), 재견적 교차집계는 negodata 에서 별도 처리.