[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:
parent
f500210cd8
commit
bbcdf78c62
@ -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():
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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}'''
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user