Skip to content

Commit e54426d

Browse files
[op] Fix kbcleaning cleaned text leakage (#505)
* Fix kbcleaning cleaned text leakage * Harden kbcleaning prompt leakage cleanup
1 parent 243bf87 commit e54426d

3 files changed

Lines changed: 86 additions & 31 deletions

File tree

dataflow/operators/knowledge_cleaning/generate/kbc_text_cleaner.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,76 @@
1111
from typing import Union
1212

1313
import re
14+
15+
16+
_CLEANED_BLOCK_RE = re.compile(
17+
r"<cleaned_start>\s*(.*?)\s*<cleaned_end>",
18+
flags=re.IGNORECASE | re.DOTALL,
19+
)
20+
_ANSWER_BLOCK_RE = re.compile(
21+
r"<answer>\s*(.*?)\s*</answer>",
22+
flags=re.IGNORECASE | re.DOTALL,
23+
)
24+
_PROCESSING_STEPS_RE = re.compile(
25+
r"\s*Processing Steps:\s*"
26+
r"1\.\s*\[Tag Analysis\]\s*Classify markup tags\s*"
27+
r"2\.\s*\[Reference Extraction\]\s*Isolate images/tables\s*"
28+
r"3\.\s*\[Character Audit\]\s*Log special chars\s*"
29+
r"4\.\s*\[Structure Check\]\s*Validate hierarchy\s*"
30+
r"5\.\s*\[Final Output\]\s*Generate cleaned text.*$",
31+
flags=re.IGNORECASE | re.DOTALL,
32+
)
33+
_PROCESSING_STEPS_WITHOUT_HEADER_RE = re.compile(
34+
r"\s*1\.\s*\[Tag Analysis\]\s*Classify markup tags\s*"
35+
r"2\.\s*\[Reference Extraction\]\s*Isolate images/tables\s*"
36+
r"3\.\s*\[Character Audit\]\s*Log special chars\s*"
37+
r"4\.\s*\[Structure Check\]\s*Validate hierarchy\s*"
38+
r"5\.\s*\[Final Output\]\s*Generate cleaned text.*$",
39+
flags=re.IGNORECASE | re.DOTALL,
40+
)
41+
_ZH_PROCESSING_STEPS_RE = re.compile(
42+
r"\s*(?:处理步骤|处理流程|治理步骤)[::]\s*"
43+
r"1[.、]\s*\[(?:标签分析|标记分析)\].*?"
44+
r"2[.、]\s*\[(?:引用提取|参考提取)\].*?"
45+
r"3[.、]\s*\[(?:字符审核|字符审计)\].*?"
46+
r"4[.、]\s*\[(?:结构检查|结构校验)\].*?"
47+
r"5[.、]\s*\[(?:最终输出|最终结果)\].*$",
48+
flags=re.DOTALL,
49+
)
50+
_ZH_PROCESSING_STEPS_WITHOUT_HEADER_RE = re.compile(
51+
r"\s*1[.、]\s*\[(?:标签分析|标记分析)\].*?"
52+
r"2[.、]\s*\[(?:引用提取|参考提取)\].*?"
53+
r"3[.、]\s*\[(?:字符审核|字符审计)\].*?"
54+
r"4[.、]\s*\[(?:结构检查|结构校验)\].*?"
55+
r"5[.、]\s*\[(?:最终输出|最终结果)\].*$",
56+
flags=re.DOTALL,
57+
)
58+
59+
60+
def extract_cleaned_text(text, post_process=None) -> str:
61+
"""Extract model output while dropping prompt-instruction leakage."""
62+
text = "" if text is None else str(text)
63+
64+
cleaned_match = _CLEANED_BLOCK_RE.search(text)
65+
if cleaned_match:
66+
text = cleaned_match.group(1)
67+
else:
68+
answer_match = _ANSWER_BLOCK_RE.search(text)
69+
if answer_match:
70+
text = answer_match.group(1)
71+
text = text.replace("<cleaned_start>", "").replace("<cleaned_end>", "")
72+
73+
text = _PROCESSING_STEPS_RE.sub("", text)
74+
text = _PROCESSING_STEPS_WITHOUT_HEADER_RE.sub("", text)
75+
text = _ZH_PROCESSING_STEPS_RE.sub("", text)
76+
text = _ZH_PROCESSING_STEPS_WITHOUT_HEADER_RE.sub("", text)
77+
text = text.strip()
78+
79+
if post_process:
80+
text = post_process(text)
81+
return text
82+
83+
1484
@prompt_restrict(
1585
KnowledgeCleanerPrompt
1686
)
@@ -27,7 +97,7 @@ def __init__(self, llm_serving: LLMServingABC, lang="en", prompt_template : Unio
2797
if prompt_template:
2898
self.prompt_template = prompt_template
2999
else:
30-
self.prompt_template = KnowledgeCleanerPrompt()
100+
self.prompt_template = KnowledgeCleanerPrompt(lang=lang)
31101

32102
@staticmethod
33103
def get_desc(lang: str = "zh"):
@@ -123,15 +193,16 @@ def run(
123193
formatted_prompts = self._reformat_prompt(dataframe)
124194
cleaned = self.llm_serving.generate_from_input(formatted_prompts,"")
125195

126-
#for each in cleaned, only save the content in <cleaned_start> and <cleaned_end>
196+
# Save only the final cleaned text, even if the model leaks prompt steps.
127197
cleaned_extracted = [
128-
str(text).split('<cleaned_start>')[1].split('<cleaned_end>')[0].strip()
129-
if '<cleaned_start>' in str(text) and '<cleaned_end>' in str(text)
130-
else str(text).strip()
198+
extract_cleaned_text(
199+
text,
200+
getattr(self.prompt_template, "_post_process", None),
201+
)
131202
for text in cleaned
132203
]
133204
dataframe[self.output_key] = cleaned_extracted
134205
output_file = storage.write(dataframe)
135206
self.logger.info(f"Results saved to {output_file}")
136207

137-
return [output_key]
208+
return [output_key]

dataflow/operators/knowledge_cleaning/generate/kbc_text_cleaner_batch.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataflow.prompts.kbcleaning import KnowledgeCleanerPrompt
2+
from dataflow.operators.knowledge_cleaning.generate.kbc_text_cleaner import extract_cleaned_text
23
import pandas as pd
34
from dataflow.utils.registry import OPERATOR_REGISTRY
45
from dataflow import get_logger
@@ -125,11 +126,12 @@ def run(
125126
raw_chunks, formatted_prompts = self._reformat_prompt_from_path(chunk_path)
126127
cleaned = self.llm_serving.generate_from_input(formatted_prompts, "")
127128

128-
# for each in cleaned, only save the content in <cleaned_start> and <cleaned_end>
129+
# Save only the final cleaned text, even if the model leaks prompt steps.
129130
cleaned_extracted = [
130-
text.split('<cleaned_start>')[1].split('<cleaned_end>')[0].strip()
131-
if '<cleaned_start>' in str(text) and '<cleaned_end>' in str(text)
132-
else str(text).strip()
131+
extract_cleaned_text(
132+
text,
133+
getattr(self.prompts, "_post_process", None),
134+
)
133135
for text in cleaned
134136
]
135137
json_items=[{

dataflow/prompts/kbcleaning.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -177,35 +177,17 @@ def build_prompt(self, raw_content: str) -> str:
177177
"""
178178

179179
if self.lang == "en":
180-
processing_steps = """
181-
Processing Steps:
182-
1. [Tag Analysis] Classify markup tags
183-
2. [Reference Extraction] Isolate images/tables
184-
3. [Character Audit] Log special chars
185-
4. [Structure Check] Validate hierarchy
186-
5. [Final Output] Generate cleaned text
187-
""".strip()
188-
output_requirement = 'Response must contain ONLY cleaned text between <cleaned_start> and <cleaned_end>.'
180+
output_requirement = 'Response must contain ONLY the final cleaned text between <cleaned_start> and <cleaned_end>. Do not include analysis, explanations, processing steps, or bullet lists outside the cleaned text.'
189181
else:
190-
processing_steps = """
191-
处理步骤:
192-
1. [标签分析] 识别并分类所有标记标签
193-
2. [引用提取] 分离图片/表格/签名等引用内容
194-
3. [字符审核] 记录特殊字符变更
195-
4. [结构检查] 验证文本层级
196-
5. [最终输出] 生成清洗后文本
197-
""".strip()
198182
output_requirement = '响应必须只包含清洗后文本,以<cleaned_start>开头,<cleaned_end>结尾,无其他内容。'
199183

200184
return f"""
201185
{self.prompt_header}
202186
187+
{output_requirement}
188+
203189
待清洗内容:
204190
{raw_content}
205-
206-
{processing_steps}
207-
208-
{output_requirement}
209191
""".strip()
210192

211193
def _post_process(self, cleaned_text: str) -> str:

0 commit comments

Comments
 (0)