From bbcdf78c627a2ab07603ee704794256e12470713 Mon Sep 17 00:00:00 2001 From: hbyang Date: Tue, 22 Sep 2026 16:52:24 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20solution/backend:=20=EC=B9=B4=ED=86=A1?= =?UTF-8?q?=20=EC=9D=91=EB=8B=B5=EC=9D=B4=205=EC=B4=88=20=EB=B2=BD?= =?UTF-8?q?=EC=97=90=20=EA=B3=84=EC=86=8D=20=EA=B1=B8=EB=A6=AC=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=20=E2=80=94=20=ED=94=84=EB=A1=AC=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=95=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실사용에서 모든 발화가 4.5초 상한에 걸려 "확인하는 데 시간이 조금 걸리네요" 만 반복됐다(킹서버 로그: 4.60s · 4.51s · 4.51s). - 이미 연결된 사람이 코드를 또 보내면 LLM 을 부르지 않는다. 실제로 그랬고, 6자리가 그냥 발화로 넘어가 유료 호출 + 대기만 쌓였다 - 사이트 상태를 프롬프트에서 뺀다. 그 한 줄 때문에 매 턴 사이트 조회 + 슬러그 계산이 돌았고, 정작 필요할 때는 get_site_status 도구를 부르면 된다 - fact 는 key:value 만, 상한 30개. label 은 항목 목록에 이미 있어 두 번 보내면 프롬프트만 커지고 모델이 얻는 것이 없다 - 항목 목록도 JSON 대신 `key: 이름` 줄로 ★ 근본 해결은 콜백이다(f500210). 오픈빌더에서 '콜백 사용' 이 꺼져 있으면 callbackUrl 이 안 와서 조용히 동기 경로로만 돈다 — 지금 로그가 그 상태다. test_kakao_webhook·test_agent_runtime 43 passed Co-Authored-By: Claude Opus 5 (1M context) --- solution/backend/services/agent/channel.py | 5 +++++ solution/backend/services/agent/runtime.py | 18 +++++++++++------- solution/backend/services/prompts/agent.py | 9 +++------ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/solution/backend/services/agent/channel.py b/solution/backend/services/agent/channel.py index 28567ed..a2bacaf 100644 --- a/solution/backend/services/agent/channel.py +++ b/solution/backend/services/agent/channel.py @@ -224,6 +224,11 @@ async def handle(utterance: str, channel_user_key: str) -> dict: if user is None: return _say("계정을 찾지 못했어요. 관리자 화면에서 다시 연결해 주세요.") + # ★ 이미 연결된 사람이 코드를 또 보내는 일이 실제로 있었다(2026-09-22). 그대로 두면 + # 6자리가 그냥 발화로 모델에 넘어가 유료 호출 + 대기만 쌓인다 — 여기서 끊는다. + if CODE_PATTERN.fullmatch(utterance.upper()): + return _say("이미 연결되어 있어요. 바로 말씀하시면 됩니다.\n예) 체크인 시간 3시로 바꿔줘") + # ── 확인 이어받기 ──────────────────────────────────────────────────── pending = None if row.pending_tool and row.pending_expires_at and row.pending_expires_at > _now(): diff --git a/solution/backend/services/agent/runtime.py b/solution/backend/services/agent/runtime.py index 1d53f68..ee1e352 100644 --- a/solution/backend/services/agent/runtime.py +++ b/solution/backend/services/agent/runtime.py @@ -71,14 +71,18 @@ async def _context_facts(user: UserInfo, place_id: str, place) -> list[dict]: res = await FactService(FactCRUD(), PlaceCRUD()).list_facts(user, place_id, publishable_only=True) schema = get_schema(PlaceCategory(place.category)) out = [] - for f in (res.facts or [])[:60]: + for f in (res.facts or []): spec = schema.get(f.key) if spec and spec.scope == "place" and (f.value or "").strip(): - out.append({"key": f.key, "label": spec.label, "value": f.value}) - return out + # ★ label 은 싣지 않는다 — 아래 '항목 목록' 에 이미 key↔label 이 있다. + # 같은 표를 두 번 보내면 프롬프트만 커지고 모델이 얻는 것은 없다. + out.append({f.key: f.value}) + # ★ 상한을 둔다. 실측(2026-09-22): 필드 43 + fact 수십 개가 실린 프롬프트가 5초 벽을 + # 넘겼다. 무한정 싣지 않는다 — 대화 한 턴에 필요한 맥락은 그렇게 많지 않다. + return out[:30] -async def _choose(place, fields, facts, site_line, message) -> dict: +async def _choose(place, fields, facts, message) -> dict: """LLM 한 번. 고른 도구 이름과 인자만 받는다.""" active = provider.active() async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client: @@ -90,7 +94,6 @@ async def _choose(place, fields, facts, site_line, message) -> dict: tools=registry.describe(), fields=fields, facts=facts, - site={"요약": site_line}, message=message, ), response_schema=prompt.RESPONSE_SCHEMA, @@ -126,10 +129,11 @@ async def chat(user: UserInfo, place_id: str, message: str, confirm: dict | None fields = registry.fields_of(place) facts = await _context_facts(user, place_id, place) - site_line = await registry.REGISTRY["get_site_status"].run(ctx, {}) + # ★ 사이트 상태는 프롬프트에 싣지 않는다. 그 한 줄 때문에 매 턴 사이트 조회 + 슬러그 계산이 + # 돌았고, 정작 모델이 필요할 때는 `get_site_status` 도구를 부르면 된다. try: - choice = await _choose(place, fields, facts, site_line, message) + choice = await _choose(place, fields, facts, message) except LlmError as ex: LOG.w(f"[agent] 도구 선택 실패: {type(ex).__name__}") raise AgentError("AGENT_CALL_FAILED") from ex diff --git a/solution/backend/services/prompts/agent.py b/solution/backend/services/prompts/agent.py index 4d3f0af..a1bd76b 100644 --- a/solution/backend/services/prompts/agent.py +++ b/solution/backend/services/prompts/agent.py @@ -36,7 +36,7 @@ RESPONSE_SCHEMA = { } -def build_prompt(*, place_name: str, tools: list[dict], fields: list[dict], facts: list[dict], site: dict, message: str) -> str: +def build_prompt(*, place_name: str, tools: list[dict], fields: list[dict], facts: list[dict], message: str) -> str: """사장님 발화 → 도구 하나. ★ 모호하면 실행하지 말고 되물으라고 명시한다. 티오더가 "유사한 메뉴가 2개 이상이면 @@ -55,14 +55,11 @@ def build_prompt(*, place_name: str, tools: list[dict], fields: list[dict], fact 쓸 수 있는 도구: {json.dumps(tools, ensure_ascii=False, indent=1)} -가게 정보에 쓸 수 있는 항목(set_fact 의 key 는 반드시 이 중 하나다): -{json.dumps(fields, ensure_ascii=False)} +가게 정보에 쓸 수 있는 항목 — `key: 이름` (set_fact 의 key 는 반드시 이 중 하나다): +{chr(10).join(f"{f['key']}: {f['label']}" for f in fields)} 지금 저장된 값: {json.dumps(facts, ensure_ascii=False)} -사이트 상태: -{json.dumps(site, ensure_ascii=False)} - 사장님 요청: {message}'''