Skip to content

Commit 64d23c2

Browse files
i3monthsclaude
andcommitted
fix(ai): 꼬리질문에 '<' 포함 시 스트리밍 중 잘림 수정
extract_question_span 의 '미완성 태그 꼬리 제거' 휴리스틱이 질문 본문의 부등호·제네릭('a < b', 'List<T>')까지 잘라내, 스트리밍 중 질문이 '<' 지점에서 멈춰 잘린 채로 표시되던 문제(닫는 태그 도착 전까지 지속). - 마지막 '<' 꼬리가 </question> 또는 <meta> 시작과 일치할 때만 제거하도록 보수화 → 본문의 '<' 는 보존 - 닫는 태그 누락 시 최종 저장값 잘림도 함께 방지 - 회귀 테스트: '<' 보존, partial 태그 제거, 청크 분할 스트리밍 델타 온전성 재현/검증: extract_question_span('…<question>a < b 인지 확인하세요') 이전 'a' → 이후 'a < b 인지 확인하세요'. 전체 245 테스트 통과. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8cb7f58 commit 64d23c2

3 files changed

Lines changed: 68 additions & 3 deletions

File tree

ai/src/ai_server/chain/followup_generation_chain.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,14 @@ def extract_question_span(text: str) -> str:
3939
return ""
4040
tail = text[open_idx + len("<question>") :]
4141
tail = tail.split("<meta>", 1)[0]
42-
# 닫는/메타 태그가 스트림 청크 경계로 잘려 들어와도(예: "질문?</quest")
43-
# 미완성 '<...' 꼬리를 잘라낸다 — 질문 본문에는 '<' 가 없다고 가정.
42+
# 닫는/메타 태그가 스트림 청크 경계로 잘려 들어온 경우(예: "질문?</quest")만 그
43+
# partial 을 제거한다. 질문 본문의 부등호·제네릭('a < b', 'List<T>')까지 잘라내지
44+
# 않도록, 마지막 '<' 꼬리가 태그 시작과 일치할 때만 자른다.
4445
lt = tail.rfind("<")
4546
if lt != -1 and ">" not in tail[lt:]:
46-
tail = tail[:lt]
47+
partial = tail[lt:]
48+
if "</question>".startswith(partial) or "<meta>".startswith(partial):
49+
tail = tail[:lt]
4750
return tail.strip()
4851

4952

ai/tests/test_followup_parse.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,36 @@ def test_extract_question_only_helper():
2929
extract_question_span("<intent>NORMAL</intent><question>안녕</question><meta>{}")
3030
== "안녕"
3131
)
32+
33+
34+
def test_streaming_keeps_lt_in_body():
35+
# 스트리밍 중(닫는 태그 전) 질문 본문의 부등호·제네릭 '<' 를 잘라내면 안 된다.
36+
from ai_server.chain.followup_generation_chain import extract_question_span
37+
38+
assert (
39+
extract_question_span("<intent>NORMAL</intent><question>a < b 인지 확인하세요")
40+
== "a < b 인지 확인하세요"
41+
)
42+
assert (
43+
extract_question_span("<intent>NORMAL</intent><question>제네릭 List<T> 를 설명하세요")
44+
== "제네릭 List<T> 를 설명하세요"
45+
)
46+
47+
48+
def test_streaming_strips_partial_closing_or_meta_tag():
49+
# 청크 경계로 잘려 들어온 닫는/메타 태그 partial 만 제거한다.
50+
from ai_server.chain.followup_generation_chain import extract_question_span
51+
52+
assert extract_question_span("<intent>NORMAL</intent><question>질문입니다?</quest") == "질문입니다?"
53+
assert extract_question_span("<intent>NORMAL</intent><question>끝났나요?<met") == "끝났나요?"
54+
55+
56+
def test_parses_question_with_lt_when_closed():
57+
# 닫는 태그가 있으면 '<' 포함 질문이 그대로 보존된다(최종 저장값 잘림 방지).
58+
text = (
59+
"<intent>NORMAL</intent>"
60+
"<question>리스트에서 a < b 를 만족하는 쌍을 어떻게 셀까요?</question>"
61+
"<meta>{}</meta>"
62+
)
63+
r = parse_followup_result(text)
64+
assert r.followup_question == "리스트에서 a < b 를 만족하는 쌍을 어떻게 셀까요?"

ai/tests/test_followup_streaming.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,35 @@ async def test_stream_emits_question_tokens_and_final_result():
4848
assert result.followup_question == "그 설계에서 트레이드오프는?"
4949

5050

51+
@pytest.mark.asyncio
52+
async def test_stream_emits_full_question_with_lt_operator():
53+
# 부등호 '<' 가 든 질문이 스트리밍 중 '<' 에서 잘리지 않고 끝까지 흘러야 한다.
54+
pieces = [
55+
"<intent>NORMAL</intent><question>리스트에서 a ",
56+
"< b 를 ",
57+
"만족하는 쌍을 세는 법은?",
58+
"</question><meta>{}</meta>",
59+
]
60+
prompt = ChatPromptTemplate.from_messages([("human", "{answer_text}")])
61+
gen = StreamingFollowupGenerator(prompt=prompt, llm=_FakeStreamLLM(pieces))
62+
63+
seen = []
64+
result = await gen.stream(
65+
on_question_token=lambda piece: seen.append(piece),
66+
job_category="BACKEND",
67+
mode="TECHNICAL",
68+
previous_question="q",
69+
answer_text="a",
70+
context="(none)",
71+
parent_category="X",
72+
expected_signal="(none)",
73+
history="(none)",
74+
)
75+
joined = "".join(seen)
76+
assert joined == "리스트에서 a < b 를 만족하는 쌍을 세는 법은?"
77+
assert result.followup_question == "리스트에서 a < b 를 만족하는 쌍을 세는 법은?"
78+
79+
5180
@pytest.mark.asyncio
5281
async def test_stream_dont_know_emits_no_question_tokens():
5382
pieces = ["<intent>DONT_KNOW</intent><question>모르면 이렇게</question><meta>{}</meta>"]

0 commit comments

Comments
 (0)