[fix] solution/backend: 카톡 응답이 5초 벽에 계속 걸리던 것 — 프롬프트 축소

실사용에서 모든 발화가 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) <noreply@anthropic.com>
This commit is contained in:
hbyang 2026-09-22 16:52:24 +09:00
parent f500210cd8
commit bbcdf78c62
3 changed files with 19 additions and 13 deletions

View File

@ -224,6 +224,11 @@ async def handle(utterance: str, channel_user_key: str) -> dict:
if user is None: if user is None:
return _say("계정을 찾지 못했어요. 관리자 화면에서 다시 연결해 주세요.") return _say("계정을 찾지 못했어요. 관리자 화면에서 다시 연결해 주세요.")
# ★ 이미 연결된 사람이 코드를 또 보내는 일이 실제로 있었다(2026-09-22). 그대로 두면
# 6자리가 그냥 발화로 모델에 넘어가 유료 호출 + 대기만 쌓인다 — 여기서 끊는다.
if CODE_PATTERN.fullmatch(utterance.upper()):
return _say("이미 연결되어 있어요. 바로 말씀하시면 됩니다.\n예) 체크인 시간 3시로 바꿔줘")
# ── 확인 이어받기 ──────────────────────────────────────────────────── # ── 확인 이어받기 ────────────────────────────────────────────────────
pending = None pending = None
if row.pending_tool and row.pending_expires_at and row.pending_expires_at > _now(): if row.pending_tool and row.pending_expires_at and row.pending_expires_at > _now():

View File

@ -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) res = await FactService(FactCRUD(), PlaceCRUD()).list_facts(user, place_id, publishable_only=True)
schema = get_schema(PlaceCategory(place.category)) schema = get_schema(PlaceCategory(place.category))
out = [] out = []
for f in (res.facts or [])[:60]: for f in (res.facts or []):
spec = schema.get(f.key) spec = schema.get(f.key)
if spec and spec.scope == "place" and (f.value or "").strip(): if spec and spec.scope == "place" and (f.value or "").strip():
out.append({"key": f.key, "label": spec.label, "value": f.value}) # ★ label 은 싣지 않는다 — 아래 '항목 목록' 에 이미 key↔label 이 있다.
return out # 같은 표를 두 번 보내면 프롬프트만 커지고 모델이 얻는 것은 없다.
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 한 번. 고른 도구 이름과 인자만 받는다.""" """LLM 한 번. 고른 도구 이름과 인자만 받는다."""
active = provider.active() active = provider.active()
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client: 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(), tools=registry.describe(),
fields=fields, fields=fields,
facts=facts, facts=facts,
site={"요약": site_line},
message=message, message=message,
), ),
response_schema=prompt.RESPONSE_SCHEMA, 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) fields = registry.fields_of(place)
facts = await _context_facts(user, place_id, place) facts = await _context_facts(user, place_id, place)
site_line = await registry.REGISTRY["get_site_status"].run(ctx, {}) # ★ 사이트 상태는 프롬프트에 싣지 않는다. 그 한 줄 때문에 매 턴 사이트 조회 + 슬러그 계산이
# 돌았고, 정작 모델이 필요할 때는 `get_site_status` 도구를 부르면 된다.
try: try:
choice = await _choose(place, fields, facts, site_line, message) choice = await _choose(place, fields, facts, message)
except LlmError as ex: except LlmError as ex:
LOG.w(f"[agent] 도구 선택 실패: {type(ex).__name__}") LOG.w(f"[agent] 도구 선택 실패: {type(ex).__name__}")
raise AgentError("AGENT_CALL_FAILED") from ex raise AgentError("AGENT_CALL_FAILED") from ex

View File

@ -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개 이상이면 모호하면 실행하지 말고 되물으라고 명시한다. 티오더가 "유사한 메뉴가 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)} {json.dumps(tools, ensure_ascii=False, indent=1)}
가게 정보에 있는 항목(set_fact key 반드시 하나다): 가게 정보에 있는 항목 `key: 이름` (set_fact key 반드시 하나다):
{json.dumps(fields, ensure_ascii=False)} {chr(10).join(f"{f['key']}: {f['label']}" for f in fields)}
지금 저장된 : 지금 저장된 :
{json.dumps(facts, ensure_ascii=False)} {json.dumps(facts, ensure_ascii=False)}
사이트 상태:
{json.dumps(site, ensure_ascii=False)}
사장님 요청: 사장님 요청:
{message}''' {message}'''