docs(chat): AGENT_HANDOFF 갱신 — agent 작업/계약 의존성 정리
- ⑥ 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>
This commit is contained in:
parent
f7f5439042
commit
a3b8610336
186
AGENT_HANDOFF_CHAT_FIXES.md
Normal file
186
AGENT_HANDOFF_CHAT_FIXES.md
Normal file
@ -0,0 +1,186 @@
|
||||
# Chat 오류 수정 — agent 측 핸드오프
|
||||
|
||||
> 작성: backend/frontend 담당. 대상: **agent(9500) 담당자**.
|
||||
> 협상 진행 중 발생하던 오류(대화 막힘·오진행·desync)를 분석해 ①~⑤로 정리했다.
|
||||
> backend/frontend 에서 고칠 수 있는 부분은 이미 반영했고(아래 "backend/front 완료"),
|
||||
> **agent 코드 수정이 필요한 항목만 이 문서로 넘긴다.** 관련 계약은 [AGENT_INTEGRATION.md](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.py` HttpAgentClient body, `chat_service._agent_context`)
|
||||
- frontend 는 desync 의심 에러(1404/1405) 시 `messages` 를 다시 불러와 화면을 서버 기준으로 리싱크한다.
|
||||
|
||||
**agent 가 해줘야 할 일** ⚠️
|
||||
1. `POST /v1/chat` 요청에 새로 추가된 `client_step`(Optional[str]) 을 받는다.
|
||||
2. agent 세션(`learning.chat_sessions.step`)과 `client_step` 이 다르면 **desync** 다. 다음 중 하나로 대응:
|
||||
- (권장) agent 가 자기 step 을 정답으로 보고 응답하되, 응답에 현재 `step`/`client_step` 을 정확히 실어 보내 backend/front 가 따라오게 한다. (이미 `Res_Chat.step` 있음 — 항상 신뢰 가능한 값으로 채울 것)
|
||||
- 또는 desync 가 크면 명시적 에러/리셋 신호를 주어 backend 가 해당 세션을 재동기화하게 한다.
|
||||
3. (대안) **세션 상태 조회 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 가 해줘야 할 일** ⚠️
|
||||
1. `POST /v1/chat` 을 **멱등(idempotent)** 하게 만든다. 같은 `session_id` + 같은 턴(예: backend 가 보낼 `client_step` 또는 멱등키)에 대한
|
||||
재요청은 **이미 처리한 결과를 그대로 반환**하고 step 을 다시 전진시키지 않는다.
|
||||
- 멱등키 후보: `(session_id, client_step, user_input)` 또는 backend 가 헤더로 보낼 `Idempotency-Key`(요청 시 합의 필요).
|
||||
2. 멱등이 어렵다면 ①-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 스키마/시드/배선을 확정해야 한다:
|
||||
|
||||
1. **각 필드의 정확한 의미·단위·출처 정의**
|
||||
- `revenue_amount`(매출액): *누구의* 매출인가(거래처/공급사/품목 단위?), 단위(원), 어느 시점 값인가.
|
||||
- `distribution_code`(유통 코드): 우리가 어떤 분류로 채울지 + **agent `config code_map` 의 유효 키 목록**(없는 코드면 agent 가 `ValueError` → RL skip). 코드맵 공유 필수.
|
||||
- `acceptance_ratio`(가격 수용률 0~1): 산출 정의(과거 협상 이력 집계? 어느 기간/단위?) — 집계 로직 주체(backend/agent) 합의.
|
||||
2. **합의 후 backend 작업**: 위 정의에 맞춰 스키마 컬럼 추가(예: `quotation_settings` / 신규 테이블) + 시드 + `_agent_context` 배선.
|
||||
3. **(별개) 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 가 해줘야 할 일** ⚠️
|
||||
1. `Res_Chat` 에 **`bot_chat_type: Optional[str]`** 추가하고 각 step 에서 정확히 채울 것
|
||||
(`summaryRSP`/`summaryCM`/`rejectRSP`/`rejectCM`/`indicator`, 일반 텍스트는 None/`text`).
|
||||
- 이러면 backend 가 step 이름을 추측하지 않으므로, agent 가 step 명을 바꿔도 폼이 안 깨진다.
|
||||
2. `Res_Chat` 에 **`indicator_value: Optional[float]`(1~99)** 추가하고 `가격협상`(카드선택) 턴에 채울 것.
|
||||
⚠️ backend 컬럼이 `NUMERIC(8,6)`(절대값 <100)이라 **반드시 1~99 범위**(100 금지). (가능하면 `indicator_range` PZ1/2/3 도)
|
||||
3. (선택) 종료 폼 단계(`협상완료` 등)는 `chat_end=False` + `bot_chat_type=summaryXXX` 로, 실제 종료는 다음 `협상종료` 턴 `chat_end=True` 로 — 현 흐름 유지면 OK.
|
||||
|
||||
---
|
||||
|
||||
## ⑧ backend 수정으로 생긴 agent 계약 의존성 (agent 가 깨지 말아야 할 것)
|
||||
|
||||
이번 backend 정리(use_mock 제거, anchor/배송 배선, 응답 passthrough)로 agent 응답에 대한 **묵시적 의존성**이 생겼다.
|
||||
agent 가 아래를 바꾸면 backend 기능이 조용히 깨진다 — **업데이트라기보단 "유지 필요" 항목**이다.
|
||||
|
||||
1. **`배송형태선택` 단계는 응답 `input_mode` 를 정확히 `"delivery_type"` 으로 보낼 것.**
|
||||
- backend 는 재견적 요약(summaryCM)의 배송형태(`delivery_type`)를, **`input_mode=="delivery_type"` 인 봇 메시지 직후의 유저 선택 라벨**로 캡처한다(`chat_service._delivery_choice`).
|
||||
- 이 step 의 `input_mode` 를 다른 값으로 바꾸면 배송형태가 요약에 안 담긴다. `input_options` 라벨(협력사배송/지정택배배송/픽업배송)도 유지 권장(프론트 표시·매핑 기준).
|
||||
|
||||
2. **`anchor_price` 는 backend 가 계산해 보내므로 agent 는 받은 값을 그대로 쓸 것(재계산/덮어쓰기 금지).**
|
||||
- backend 가 견적설정 `quotation_settings.anchoring_value` 로 `anchor = round(target*(1-value))` 를 계산해 요청 body 에 넣는다.
|
||||
- agent 가 자체 `anchor_for(target)` 로 다시 계산하면 backend 와 어긋난다. 요청의 `anchor_price` 를 신뢰할 것.
|
||||
|
||||
3. **backend 에 더 이상 mock 이 없다 → agent 가 반드시 떠 있어야 한다.**
|
||||
- `use_mock`/`MockAgentClient` 제거됨. agent 미기동/오류 시 chat 은 `CHAT_AGENT_UNAVAILABLE(1402)` 로 degrade(프론트 toast). 로컬/CI 에서도 실제 agent 연동 전제.
|
||||
|
||||
4. **(재확인) `partner_count` 는 backend 가 '견적당 세션 수'로 산출해 보낸다** — agent 는 받은 값으로 partner 차원(single/multiple/none)만 판정.
|
||||
|
||||
---
|
||||
|
||||
## 변경된 `POST /v1/chat` 요청 body (backend → agent)
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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 에서 별도 처리.
|
||||
Loading…
Reference in New Issue
Block a user