更新校对错误,修改模型

This commit is contained in:
zhangkunxiang 2026-07-29 09:21:54 +08:00
parent aa4406036e
commit 37da107ade
3 changed files with 542 additions and 62 deletions

425
app.py
View File

@ -88,6 +88,7 @@ FastAPI 主 Agent 接口 - 修复重复执行问题
""" """
import logging import logging
import unicodedata import unicodedata
from difflib import SequenceMatcher
from time import sleep from time import sleep
import aiofiles import aiofiles
@ -121,7 +122,22 @@ from checkpointer_config import (
CheckpointerManager, CheckpointerManager,
checkpointer_manager checkpointer_manager
) )
from gw_write import cotprompt,build_review_prompt,length_convert,length_display,build_length_instruction,dedupe_and_filter,filter_positive_word_false_positives,append_dictionary_results,find_keywords,normalize_words,process_rag_prompt from gw_write import (
REVIEW_TYPE_ERROR_MAP,
cotprompt,
build_review_prompt,
length_convert,
length_display,
build_length_instruction,
dedupe_and_filter,
filter_positive_word_false_positives,
append_dictionary_results,
find_keywords,
normalize_words,
process_rag_prompt,
get_allowed_review_error_types,
normalize_review_types,
)
app = FastAPI(max_request_size=1024 * 1024 * 10) app = FastAPI(max_request_size=1024 * 1024 * 10)
_doc2pdf_converter: Optional[Doc2PDF] = None _doc2pdf_converter: Optional[Doc2PDF] = None
@ -147,7 +163,8 @@ REWRITE_TYPE_MAP = {
REVIEW_CHUNK_SIZE = 1000 REVIEW_CHUNK_SIZE = 1000
REVIEW_MAX_CONCURRENCY = 5 REVIEW_MAX_CONCURRENCY = 5
REVIEW_SYSTEM_PROMPT = ( REVIEW_SYSTEM_PROMPT = (
"你是一个严谨、穷尽式的专业文本校对专家,必须完整检查全文并返回所有符合规则的问题,不能只挑选部分问题。" "你是一个严谨的专业文本校对专家,必须完整检查用户明确选中的校对范围,并返回该范围内的真实问题。"
"禁止检查或输出用户未选中的校对类别,禁止重复输出相同问题,原文与建议相同时不得输出。"
"校对结果会由程序直接替换进正式公文,因此建议字段只能包含最终正文,任何解释、括号备注或操作说明都会污染公文,绝对禁止输出。" "校对结果会由程序直接替换进正式公文,因此建议字段只能包含最终正文,任何解释、括号备注或操作说明都会污染公文,绝对禁止输出。"
"每条原文字段必须是输入正文中真实存在的连续原样子串,不能包含任何提前修改后的文字,否则该结果会被系统丢弃。" "每条原文字段必须是输入正文中真实存在的连续原样子串,不能包含任何提前修改后的文字,否则该结果会被系统丢弃。"
"请严格按照用户要求的格式输出。" "请严格按照用户要求的格式输出。"
@ -311,8 +328,28 @@ WRITE_FORMULA_INSTRUCTION = r"""
7. 若正文不需要公式不要输出任何 `$` `$$` 7. 若正文不需要公式不要输出任何 `$` `$$`
""" """
WRITE_HEADING_LAYOUT_INSTRUCTION = """
标题与正文换行规范必须遵守
1. 形式的一级标题和形式的二级标题必须分别独占一行严禁把一级标题二级标题或正文连续写在同一行
2. 每个二级标题后必须换行正文从下一段开始正文结束后再另起一行输出下一个二级标题
3. 标题与正文之间相邻层级标题之间使用一个空行分隔以便前端 Markdown 正确显示
4. 必须按以下结构输出不得写成标题 正文
一级标题
二级标题
正文内容
二级标题
正文内容
"""
WRITE_SYSTEM_PROMPT = ( WRITE_SYSTEM_PROMPT = (
"你是专业公文写作助手,请直接输出正文内容。" "你是专业公文写作助手,请直接输出正文内容。"
"一级标题与二级标题必须分别独占一行,每个二级标题后的正文必须另起一段。"
"输出使用可由前端 Markdown 和 KaTeX 直接解析的内容;如需公式,必须严格遵守用户提示中的公式输出规范。" "输出使用可由前端 Markdown 和 KaTeX 直接解析的内容;如需公式,必须严格遵守用户提示中的公式输出规范。"
) )
@ -372,7 +409,12 @@ def build_write_prompt(request: WriteRequest) -> str:
first_level_count, first_level_count,
) )
return document_prompt + structure_instruction + WRITE_FORMULA_INSTRUCTION return (
document_prompt
+ structure_instruction
+ WRITE_HEADING_LAYOUT_INSTRUCTION
+ WRITE_FORMULA_INSTRUCTION
)
async def stream_write_content(prompt: str): async def stream_write_content(prompt: str):
@ -450,6 +492,16 @@ class ReviewRequest(BaseModel):
def check_content(self): def check_content(self):
if not self.content or not self.content.strip(): if not self.content or not self.content.strip():
raise ValueError("content不能为空") raise ValueError("content不能为空")
normalized_types = normalize_review_types(self.types)
unknown_types = [
review_type for review_type in self.types
if review_type not in REVIEW_TYPE_ERROR_MAP
]
if unknown_types:
raise ValueError(f"不支持的检查类型:{''.join(dict.fromkeys(unknown_types))}")
if not normalized_types:
raise ValueError("types不能为空")
self.types = normalized_types
return self return self
@ -472,6 +524,12 @@ _REVIEW_BLOCK_RE = re.compile(
r"建议[:]\s*(?P<suggestion>.*?)(?=\n\s*\n|$)", r"建议[:]\s*(?P<suggestion>.*?)(?=\n\s*\n|$)",
re.S, re.S,
) )
_COMPLETED_REVIEW_BLOCK_RE = re.compile(
r"错误[:]\s*(?P<error>.*?)\s*[\r\n]+"
r"原文[:]\s*(?P<original>.*?)\s*[\r\n]+"
r"建议[:]\s*(?P<suggestion>.*?)(?=\r?\n\s*\r?\n|\r?\n?###)",
re.S,
)
def parse_review_output(text: str) -> List[dict]: def parse_review_output(text: str) -> List[dict]:
text = text.strip() text = text.strip()
@ -497,40 +555,302 @@ def parse_review_output(text: str) -> List[dict]:
return result return result
def _has_repeating_review_tail(text: str) -> bool:
"""检测模型是否开始连续重复同一条完整记录。"""
matches = list(_COMPLETED_REVIEW_BLOCK_RE.finditer(text))
if len(matches) < 2:
return False
def record_key(match) -> tuple:
return (
match.group("error").strip(),
match.group("original").strip(),
match.group("suggestion").strip(),
)
return record_key(matches[-1]) == record_key(matches[-2])
def _split_review_error_types(error: str) -> List[str]: def _split_review_error_types(error: str) -> List[str]:
"""按原顺序拆分并去重校对错误类型。""" """按原顺序拆分并去重校对错误类型。"""
aliases = {
"逻辑错误": "逻辑不通",
"逻辑不顺": "逻辑不通",
"错别字符": "错别字",
}
result = [] result = []
for error_type in re.split(r"[、,]", error or ""): for error_type in re.split(r"[、,]", error or ""):
error_type = error_type.strip() error_type = aliases.get(error_type.strip(), error_type.strip())
if error_type and error_type not in result: if error_type and error_type not in result:
result.append(error_type) result.append(error_type)
return result return result
def _is_whitespace_only_change(original: str, suggestion: str) -> bool: def _is_whitespace_only_change(original: str, suggestion: str) -> bool:
"""判断原文与建议是否仅存在空白或末尾标点差异。""" """判断原文与建议是否相同,或仅存在空白差异。"""
if not original or not suggestion or original == suggestion: if not original or not suggestion:
return False return False
def normalize(value: str) -> str: def normalize(value: str) -> str:
# 统一全角/半角字符,避免“(4)”与“4”被误判为内容修改。
value = unicodedata.normalize("NFKC", str(value)) value = unicodedata.normalize("NFKC", str(value))
value = re.sub(r"\s+", "", value) return re.sub(r"\s+", "", value)
# 校对模型有时会给原文补充句末冒号、句号等格式标点;
# 若除此之外内容完全一致,则不作为实际错误返回。
return re.sub(r"[,。;:、,.!?:;]+$", "", value)
return normalize(original) == normalize(suggestion) return normalize(original) == normalize(suggestion)
_HEADING_PREFIX_RE = re.compile(
r"^\s*(?:"
r"[一二三四五六七八九十百]+、"
r"|[(][一二三四五六七八九十百\d]+[)]"
r"|\d+(?:\.\d+)+(?=\s*[\u4e00-\u9fff])"
r"|\d+(?:\.\d+)*[.、]"
r")"
)
_TRAILING_PUNCTUATION_RE = re.compile(r"[,。;:、,.!?:;]+$")
_FIGURE_CAPTION_RE = re.compile(
r"^\s*图\s*(?P<number>\d*)\s*[:]\s*(?P<title>.+?)\s*$"
)
def _is_standalone_heading_punctuation_change(
original: str,
suggestion: str,
) -> bool:
"""过滤只修改独占一行编号标题末尾标点的过度校对。"""
if "\n" in original or "\n" in suggestion:
return False
if not _HEADING_PREFIX_RE.match(original):
return False
original_body = _TRAILING_PUNCTUATION_RE.sub("", original.strip())
suggestion_body = _TRAILING_PUNCTUATION_RE.sub("", suggestion.strip())
return (
original_body == suggestion_body
and original.strip() != suggestion.strip()
)
def _is_figure_numbering_change(
original: str,
suggestion: str,
) -> bool:
"""
分块无法获知全文图号若标题内容未变只新增删除或修改图号
一律不作为校对结果返回
"""
original_match = _FIGURE_CAPTION_RE.match(original or "")
suggestion_match = _FIGURE_CAPTION_RE.match(suggestion or "")
if not original_match or not suggestion_match:
return False
return (
original_match.group("title").strip()
== suggestion_match.group("title").strip()
and original_match.group("number")
!= suggestion_match.group("number")
)
def _review_content_skeleton(value: str) -> str:
"""保留字母和数字,用于识别仅标点发生变化的建议。"""
normalized = unicodedata.normalize("NFKC", str(value))
return "".join(
char for char in normalized
if unicodedata.category(char)[:1] in {"L", "N"}
)
def _is_punctuation_only_change(original: str, suggestion: str) -> bool:
return (
bool(original)
and bool(suggestion)
and original != suggestion
and _review_content_skeleton(original)
== _review_content_skeleton(suggestion)
)
def _is_heading_number_change(original: str, suggestion: str) -> bool:
"""识别标题编号变化,避免模型误标为错别字或逻辑错误。"""
original_match = _HEADING_PREFIX_RE.match(original)
suggestion_match = _HEADING_PREFIX_RE.match(suggestion)
if not original_match or not suggestion_match:
return False
return original_match.group() != suggestion_match.group()
def _is_adjacent_repetition_removal(
original: str,
suggestion: str,
) -> bool:
"""识别“常见常见 -> 常见”这类明确的相邻重复删除。"""
if not original or len(original) <= len(suggestion):
return False
removed_length = len(original) - len(suggestion)
if removed_length > 50:
return False
for start in range(0, len(original) - removed_length * 2 + 1):
repeated = original[start:start + removed_length]
if (
repeated
and original[start + removed_length:start + removed_length * 2]
== repeated
and original[:start] + original[start + removed_length:]
== suggestion
):
return True
return False
_CREDENTIAL_EXPOSURE_RE = re.compile(
r"(?i)(?:"
r"(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis)://[^:\s/]+:[^@\s]+@"
r"|(?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*[^\s,;]+"
r")"
)
_COMMAND_TEXT_RE = re.compile(
r"(?i)(?:^|\s)(?:--?[a-z][\w-]*|docker(?:\s|$)|dockker(?:\s|$)|doker(?:\s|$))"
)
def _contains_credential_exposure(text: str) -> bool:
return bool(_CREDENTIAL_EXPOSURE_RE.search(text or ""))
def _is_likely_local_text_correction(
original: str,
suggestion: str,
) -> bool:
"""
识别错字漏字短语润色和命令拼写等局部修改
这类修改不能仅通过改写错误标签混入逻辑或合规单选结果
"""
if not original or not suggestion or original == suggestion:
return False
if _COMMAND_TEXT_RE.search(original):
return True
edits = [
(tag, original[i1:i2], suggestion[j1:j2])
for tag, i1, i2, j1, j2 in SequenceMatcher(
None,
original,
suggestion,
autojunk=False,
).get_opcodes()
if tag != "equal"
]
if not edits:
return False
changed_text = "".join(
original_part + suggestion_part
for _, original_part, suggestion_part in edits
)
if any(char.isdigit() for char in changed_text):
# 年份、日期、数值的前后冲突可能是真正的逻辑问题。
return False
total_changed = sum(
len(original_part) + len(suggestion_part)
for _, original_part, suggestion_part in edits
)
max_span = max(
max(len(original_part), len(suggestion_part))
for _, original_part, suggestion_part in edits
)
return total_changed <= 8 and max_span <= 4
def _minimize_multiline_review_item(
item: dict,
source_text: str,
) -> Optional[dict]:
"""
行式返回协议无法承载多行原文对于标签换行值缺少冒号的情况
名称\\n主发动机 -> 名称主发动机缩小为名称 -> 名称
其他不能安全缩小的多行修改直接丢弃避免前端只显示第一行后误替换
"""
original = str(item.get("original", "")).strip()
suggestion = str(item.get("suggestion", "")).strip()
if "\n" not in original and "\r" not in original:
return dict(item)
original_lines = [
line.strip() for line in original.splitlines()
if line.strip()
]
if len(original_lines) < 2 or "\n" in suggestion or "\r" in suggestion:
return None
original_head = original_lines[0]
unchanged_tail = "".join(original_lines[1:])
compact_suggestion = re.sub(r"\s+", "", suggestion)
if not unchanged_tail or not compact_suggestion.endswith(unchanged_tail):
return None
minimized_suggestion = compact_suggestion[:-len(unchanged_tail)]
if (
not minimized_suggestion
or minimized_suggestion == original_head
or original_head not in source_text
):
return None
minimized = dict(item)
minimized["original"] = original_head
minimized["suggestion"] = minimized_suggestion
return minimized
def _normalize_review_item_error(item: dict) -> dict:
"""让明显可判定的错误类型与实际修改保持一致。"""
normalized = dict(item)
original = str(normalized.get("original", "")).strip()
suggestion = str(normalized.get("suggestion", "")).strip()
if _contains_credential_exposure(original):
normalized["error"] = "合规问题"
elif _is_adjacent_repetition_removal(original, suggestion):
normalized["error"] = "重复内容"
elif _is_heading_number_change(original, suggestion):
normalized["error"] = "格式不规范"
elif _is_punctuation_only_change(original, suggestion):
normalized["error"] = "标点不规范"
elif _is_likely_local_text_correction(original, suggestion):
normalized["error"] = "错别词语"
else:
normalized["error"] = "".join(
_split_review_error_types(normalized.get("error", ""))
)
return normalized
def repair_review_output( def repair_review_output(
model_output: str, model_output: str,
source_text: str, source_text: str,
positive_words: Optional[Set[str]] = None, positive_words: Optional[Set[str]] = None,
sensitive_words: Optional[Set[str]] = None,
types: Optional[List[str]] = None,
) -> str: ) -> str:
"""修复提前改写原文以及修改范围重叠的校对结果。""" """修复提前改写原文以及修改范围重叠的校对结果。"""
items = parse_review_output(model_output) sensitive_word_set = sensitive_words or set()
parsed_items = parse_review_output(model_output)
items = []
for item in parsed_items:
minimized_item = _minimize_multiline_review_item(item, source_text)
if minimized_item is not None:
normalized_item = _normalize_review_item_error(minimized_item)
if (
str(minimized_item.get("original", "")).strip()
in sensitive_word_set
and "敏感词汇" in _split_review_error_types(
minimized_item.get("error", "")
)
):
normalized_item["error"] = "敏感词汇"
items.append(normalized_item)
if not items: if not items:
return model_output return "###\n###"
corrections = [ corrections = [
item for item in items item for item in items
@ -609,15 +929,59 @@ def repair_review_output(
long_item["error"] = "".join(long_error_types) long_item["error"] = "".join(long_error_types)
covered_indexes.add(short_index) covered_indexes.add(short_index)
final_items = [ allowed_error_types = (
item for index, item in enumerate(repaired_items) get_allowed_review_error_types(types)
if index not in covered_indexes if types is not None
and item.get("original", "").strip() else None
and item.get("error", "").strip() )
and not _is_whitespace_only_change( if allowed_error_types is not None and not sensitive_word_set:
item.get("original", ""), item.get("suggestion", "") allowed_error_types.discard("敏感词汇")
final_items = []
seen_records = set()
for index, item in enumerate(repaired_items):
original = item.get("original", "").strip()
suggestion = item.get("suggestion", "").strip()
error_types = _split_review_error_types(item.get("error", ""))
suggestion_is_valid = bool(suggestion) or "重复内容" in error_types
if (
index in covered_indexes
or not original
or not suggestion_is_valid
or not error_types
or original not in source_text
or (
bool(suggestion)
and _is_whitespace_only_change(original, suggestion)
)
or _is_standalone_heading_punctuation_change(original, suggestion)
or _is_figure_numbering_change(original, suggestion)
or _is_heading_number_change(original, suggestion)
):
continue
if (
allowed_error_types is not None
and any(
error_type not in allowed_error_types
for error_type in error_types
)
):
continue
if (
"敏感词汇" in error_types
and original not in sensitive_word_set
):
continue
normalized_item = dict(item)
normalized_item["error"] = "".join(error_types)
record_key = (
original,
normalized_item["error"],
suggestion,
) )
] if record_key in seen_records:
continue
seen_records.add(record_key)
final_items.append(normalized_item)
# 正词命中的“错别字”记录不视为错误,例如“维休”被定义为正词时, # 正词命中的“错别字”记录不视为错误,例如“维休”被定义为正词时,
# 不返回“维休”→“维修”的错别字建议。 # 不返回“维休”→“维修”的错别字建议。
@ -635,6 +999,8 @@ def repair_review_output(
for item in final_items for item in final_items
if item.get("original", "").strip() if item.get("original", "").strip()
] ]
if not blocks:
return "###\n###"
return "###\n" + "\n\n".join(blocks) + "\n###" return "###\n" + "\n\n".join(blocks) + "\n###"
@ -656,12 +1022,16 @@ async def call_review_model(prompt: Iterable[ChatCompletionMessageParam]) -> str
async for chunk in stream_review_model(prompt): async for chunk in stream_review_model(prompt):
full_text += chunk full_text += chunk
if _has_repeating_review_tail(full_text):
logger.warning("[公文校对] 检测到模型连续重复同一条记录,提前结束本块输出")
break
return full_text return full_text
def build_review_stream_require( def build_review_stream_require(
require: Optional[str], require: Optional[str],
types: List[str],
sensitive_words: List[str], sensitive_words: List[str],
negative_words: List[str], negative_words: List[str],
positive_words: List[str], positive_words: List[str],
@ -671,16 +1041,18 @@ def build_review_stream_require(
if require and require.strip(): if require and require.strip():
requirements.append(require.strip()) requirements.append(require.strip())
selected_types = set(normalize_review_types(types))
sensitive_text = "".join(dict.fromkeys(word.strip() for word in sensitive_words if word and word.strip())) sensitive_text = "".join(dict.fromkeys(word.strip() for word in sensitive_words if word and word.strip()))
if sensitive_text: if sensitive_text and "合规性检查" in selected_types:
requirements.append(f"请重点检查以下敏感词:{sensitive_text}。如果原文出现这些词,错误类型写“敏感词汇”。") requirements.append(f"请重点检查以下敏感词:{sensitive_text}。如果原文出现这些词,错误类型写“敏感词汇”。")
negative_text = "".join(dict.fromkeys(word.strip() for word in negative_words if word and word.strip())) negative_text = "".join(dict.fromkeys(word.strip() for word in negative_words if word and word.strip()))
if negative_text: if negative_text and "基础校对" in selected_types:
requirements.append(f"请重点检查以下错误词:{negative_text}。如果原文出现这些词,错误类型写“错误词汇”。") requirements.append(f"请重点检查以下错误词:{negative_text}。如果原文出现这些词,错误类型写“错误词汇”。")
positive_text = "".join(dict.fromkeys(word.strip() for word in positive_words if word and word.strip())) positive_text = "".join(dict.fromkeys(word.strip() for word in positive_words if word and word.strip()))
if positive_text: if positive_text and "基础校对" in selected_types:
requirements.append(f"以下词为正确词,涉及错别字判断时不要误判:{positive_text}") requirements.append(f"以下词为正确词,涉及错别字判断时不要误判:{positive_text}")
if not requirements: if not requirements:
@ -729,6 +1101,7 @@ async def stream_review_content(
): ):
review_require = build_review_stream_require( review_require = build_review_stream_require(
require=require, require=require,
types=types,
sensitive_words=sensitive_words, sensitive_words=sensitive_words,
negative_words=negative_words, negative_words=negative_words,
positive_words=positive_words, positive_words=positive_words,
@ -754,6 +1127,8 @@ async def stream_review_content(
model_output, model_output,
chunk, chunk,
positive_words=normalize_words(positive_words), positive_words=normalize_words(positive_words),
sensitive_words=normalize_words(sensitive_words),
types=types,
) )
# 仅打印过滤后的最终保留记录,不打印模型原始输出或中间结果。 # 仅打印过滤后的最终保留记录,不打印模型原始输出或中间结果。
if repaired_output.strip() and repaired_output.strip() != "###\n###": if repaired_output.strip() and repaired_output.strip() != "###\n###":
@ -1796,4 +2171,4 @@ async def root():
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=9090) uvicorn.run(app, host="0.0.0.0", port=9095)

View File

@ -13,7 +13,7 @@ load_dotenv()
# ==================== 大模型配置 ==================== # ==================== 大模型配置 ====================
LLM_CONFIG = { LLM_CONFIG = {
# "model": "Qwen3.5-35B-A3B", # "model": "Qwen3.5-35B-A3B",
"model": "46-qwen3.5-35B", "model": "46-qwen3.6-35B",
# "model": "Qwen3-14B", # "model": "Qwen3-14B",
"base_url": "http://192.168.0.46:59800/v1", "base_url": "http://192.168.0.46:59800/v1",
"api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5", "api_key": "gpustack_dee9ca823290886c_5edfc86aeeeceb1e9ee5162941cb2cb5",

View File

@ -52,11 +52,41 @@ from openai.types.chat import ChatCompletionSystemMessageParam, ChatCompletionUs
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
REVIEW_TYPE_ERROR_MAP = {
"基础校对": {
"错别字",
"错别词语",
"标点不规范",
"格式不规范",
"重复内容",
"错误词汇",
},
"逻辑校对": {"逻辑不通"},
"合规性检查": {"合规问题", "敏感词汇"},
}
REVIEW_TYPE_ORDER = tuple(REVIEW_TYPE_ERROR_MAP)
def cotprompt(query):
def normalize_review_types(types: List[str]) -> List[str]:
"""按前端传入顺序去重,只保留支持的校对类型。"""
return list(dict.fromkeys(
review_type for review_type in types
if review_type in REVIEW_TYPE_ERROR_MAP
))
def get_allowed_review_error_types(types: List[str]) -> Set[str]:
"""返回本次所选校对项允许输出的错误类型。"""
allowed = set()
for review_type in normalize_review_types(types):
allowed.update(REVIEW_TYPE_ERROR_MAP[review_type])
return allowed
def cotprompt(query, types: Optional[List[str]] = None):
# 精简 Few-Shot只示范「输入文本 -> 结构化修改结果」,不再包含推理过程/思考链, # 精简 Few-Shot只示范「输入文本 -> 结构化修改结果」,不再包含推理过程/思考链,
# 从源头杜绝模型输出解释、分析、括号备注等多余内容,同时大幅缩短每次请求的前缀长度。 # 从源头杜绝模型输出解释、分析、括号备注等多余内容,同时大幅缩短每次请求的前缀长度。
examples = [ basic_examples = [
{ {
# 综合示例:覆盖错别字/错别词语/标点不规范/重复内容, # 综合示例:覆盖错别字/错别词语/标点不规范/重复内容,
# 重点示范:多余标点(。。。)、全半角括号 -> 标点不规范(不是逻辑不通) # 重点示范:多余标点(。。。)、全半角括号 -> 标点不规范(不是逻辑不通)
@ -74,34 +104,78 @@ def cotprompt(query):
}, },
{ {
# 重复内容的原文必须带唯一上下文,建议返回删除后的完整片段。 # 重复内容的原文必须带唯一上下文,建议返回删除后的完整片段。
"question": "2026年通知2025年通知内容从下述角度开战一、项目推进方面。各部门需加快项目进度,确保按时交付。\n\n二、培训学习方面公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无辜缺席。自即日起,严禁在工作时间从事与工作无关的活动。\n\n二、纪律要求近期发现部分员工上班时间做与工作无关的事情,如炒股、聊天等,严重影响工作效率和公司形象。自即日起,严禁在工作时间从事与工作无关的活动。违反规定者,将按照公司规章制度严肃处理。\n\n特此通知:", "question": "2026年通知2026年通知内容如下\n\n一、项目推进方面\n各部门需加快项目进度,确保按时交付。\n\n二、培训学习方面\n公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无辜缺席。自即日起,严禁在工作时间从事与工作无关的活动。\n\n二、纪律要求\n近期发现部分员工上班时间做与工作无关的事情,如炒股、聊天等,严重影响工作效率和公司形象。自即日起,严禁在工作时间从事与工作无关的活动。违反规定者,将按照公司规章制度严肃处理。\n\n特此通知:",
"answer": ( "answer": (
"###\n" "###\n"
"错误:逻辑错误\n"
"原文2026年通知2025年通知内容从下述角度开战\n"
"建议2026年通知2026年通知内容从下述角度开展\n\n"
"错误:标点不规范\n"
"原文:一、项目推进方面。\n"
"建议:一、项目推进方面:\n\n"
"错误:错别词语、重复内容\n" "错误:错别词语、重复内容\n"
"原文:培训学习方面:公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无辜缺席。自即日起,严禁在工作时间从事与工作无关的活动。\n" "原文:公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无辜缺席。自即日起,严禁在工作时间从事与工作无关的活动。\n"
"建议:培训学习方面:公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无故缺席。\n\n" "建议:公司将组织系列培训课程,提升员工专业技能。具体时间、地点另行通知。员工需按时参加,不得无故缺席。\n\n"
"错误:格式不规范\n"
"原文:二、纪律要求。\n"
"建议:三、纪律要求:\n\n"
"错误:标点不规范\n" "错误:标点不规范\n"
"原文:特此通知:\n" "原文:特此通知:\n"
"建议:特此通知。\n" "建议:特此通知。\n"
"###" "###"
), ),
}, },
]
logic_examples = [
{ {
# 无明显错误示例:只返回空标记,禁止输出任何说明、分析或括号备注 "question": "项目计划于2026年1月启动并于2025年12月完成验收。",
"question": "全年审核采购合同、对账单据数百份,均按规定完成登记归档。", "answer": (
"###\n"
"错误:逻辑不通\n"
"原文项目计划于2026年1月启动并于2025年12月完成验收。\n"
"建议项目计划于2026年1月启动并于2026年12月完成验收。\n"
"###"
),
},
]
logic_scope_examples = [
{
# 单选逻辑校对时,错别字、格式和措辞润色都必须忽略。
"question": "实习培训是理论与时间结合的关键环节。3.9保秘性。第二步:再输入密码连接服务器。",
"answer": "###\n###", "answer": "###\n###",
}, },
] ]
compliance_examples = [
{
"question": "数据库连接地址为postgresql://admin:password123@10.0.0.8:5432/app。",
"answer": (
"###\n"
"错误:合规问题\n"
"原文postgresql://admin:password123@10.0.0.8:5432/app\n"
"建议postgresql://admin:***@10.0.0.8:5432/app\n"
"###"
),
},
]
compliance_scope_examples = [
{
# 普通保密、军用、核心技术等客观表述不因名称本身构成违规。
"question": "本章介绍保密资料、军用设备维修手册、未公开方案和核心技术参数。文中另有“维休”“保秘性”等错字。",
"answer": "###\n###",
},
]
selected_types = normalize_review_types(types or list(REVIEW_TYPE_ORDER))
selected_type_set = set(selected_types)
examples = []
if "基础校对" in selected_type_set:
examples.extend(basic_examples)
if "逻辑校对" in selected_type_set:
examples.extend(logic_examples)
if "合规性检查" in selected_type_set:
examples.extend(compliance_examples)
if selected_type_set == {"逻辑校对"}:
examples.extend(logic_scope_examples)
elif selected_type_set == {"合规性检查"}:
examples.extend(compliance_scope_examples)
examples.append({
"question": "全年审核采购合同、对账单据数百份,均按规定完成登记归档。",
"answer": "###\n###",
})
# 单个示例模板:只保留 问题/答案,删除“推理过程” # 单个示例模板:只保留 问题/答案,删除“推理过程”
example_prompt = PromptTemplate( example_prompt = PromptTemplate(
input_variables=["question", "answer"], input_variables=["question", "answer"],
@ -112,28 +186,29 @@ def cotprompt(query):
examples=examples, examples=examples,
example_prompt=example_prompt, example_prompt=example_prompt,
prefix=( prefix=(
"你是专业文本校对专家,负责检查错别字、错别词语、错误标点、标题格式与序号、重复内容、逻辑不通及合规问题。\n" "你是专业文本校对专家,具备检查错别字、错别词语、错误标点、标题格式、重复内容、逻辑不通及合规问题的能力;本次只执行任务要求中明确选中的校对项\n"
"输出规则(务必严格遵守):\n" "输出规则(务必严格遵守):\n"
"1. 每个错误必须连续占三行:第一行“错误:<错误类型>”,第二行“原文:<有错的原文片段>”,第三行“建议:<修改后的正确文本>”。三行之间不得插入空行;不同错误之间空一行。整体用 ### 包裹。\n" "1. 每个错误必须连续占三行:第一行“错误:<错误类型>”,第二行“原文:<有错的原文片段>”,第三行“建议:<修改后的正确文本>”。三行之间不得插入空行;不同错误之间空一行。整体用 ### 包裹。\n"
"2. 错误类型只能从以下选择:错别字、错别词语、标点不规范、格式不规范、重复内容、逻辑不通、合规问题。同一连续原文片段存在修改范围重叠的多类错误时,必须合并为一条,错误类型用中文顿号连接,并在一条建议中一次改正。标题序号重复、跳号、顺序错误或层级不一致归为“格式不规范”;标题末尾的纯标点问题归为“标点不规范”\n" "2. 错误类型只能从以下选择:错别字、错别词语、标点不规范、格式不规范、重复内容、错误词汇、逻辑不通、合规问题、敏感词汇。同一连续原文片段存在修改范围重叠的多类错误时,必须合并为一条,错误类型用中文顿号连接,并在一条建议中一次改正。\n"
"3. 凡是标点问题(多余或重复标点如“。。。”、全角/半角标点混用如英文括号()应为中文()、标点缺失或误用),一律归为“标点不规范”,禁止归为“逻辑不通”;“逻辑不通”只用于前后文语义矛盾、指代不清等真正的逻辑问题。\n" "3. 凡是标点问题(多余或重复标点如“。。。”、全角/半角标点混用如英文括号()应为中文()、标点缺失或误用),一律归为“标点不规范”,禁止归为“逻辑不通”;“逻辑不通”只用于前后文语义矛盾、指代不清等真正的逻辑问题。\n"
"4. 建议字段只写修改后的正确内容,禁止输出任何解释、分析、推理、评论或括号备注(例如禁止出现“(删除该句……)”“(此处无明显错误……)”“(通常……可接受)”这类内容)。\n" "4. 建议字段只写修改后的正确内容,禁止输出任何解释、分析、推理、评论或括号备注(例如禁止出现“(删除该句……)”“(此处无明显错误……)”“(通常……可接受)”这类内容)。原文和建议必须确有差异;两者相同或仅空白不同不得输出。相同原文不得重复输出。\n"
"5. 判定重复内容时,必须结合全文语义判断是否造成冗余,不能仅凭文字相同判错。若两处分别承担必要的说明、强调、引用或衔接作用,则不得输出校对项。\n" "5. 判定重复内容时,必须结合全文语义判断是否造成冗余,不能仅凭文字相同判错。若两处分别承担必要的说明、强调、引用或衔接作用,则不得输出校对项。\n"
"6. 确认属于重复内容后,必须比较每次出现位置与标题主题、前后文和表达作用的匹配程度,保留主题最匹配、逻辑最完整的一处,禁止机械删除第一次或最后一次。\n" "6. 确认属于重复内容后,必须比较每次出现位置与标题主题、前后文和表达作用的匹配程度,保留主题最匹配、逻辑最完整的一处,禁止机械删除第一次或最后一次。\n"
"7. 删除局部重复内容时,原文必须包含足以唯一定位的连续上下文;建议必须返回删除重复内容后的同一完整片段,不得留空。只有整个原文片段均为应删除的纯冗余内容时,建议才可留空。\n" "7. 删除局部重复内容时,原文必须包含足以唯一定位的连续上下文;建议必须返回删除重复内容后的同一完整片段,不得留空。只有整个原文片段均为应删除的纯冗余内容时,建议才可留空。\n"
"8. 必须按错别字和错别词语、标点、标题格式与序号、重复内容、逻辑、合规的顺序逐项检查全文。发现某类错误后仍须继续检查其他类别。\n" "8. 独占一行的各级标题允许不带句末标点,不得机械添加冒号或句号。只有原文明确采用“标题与正文在同一行”的格式且标点确有错误时,才校对标题末尾标点。\n"
"9. 若整段文本没有任何错误,只返回:###\n###,不要输出其它任何字符。\n" "9. 原文和建议字段都只能占一行、不得包含换行。若版式把“名称”和“主发动机”等标签与值分成多行,而实际仅缺冒号,应输出最小替换片段,例如“原文:名称”“建议:名称:”,不得把下一行值合并进建议。\n"
"10. 输入是从长文中截取的中间分块无法获知全文标题序号和图号。禁止新增、删除、替换或重排任何标题序号不得把“”改为“一、”也不得把“三、”改为“一、”。“图故障定位”这类图片说明是允许的原文格式禁止补成“图1故障定位”也禁止新增、删除或修改任何图号。标题序号与图片全局编号均不属于本次校对范围。\n"
"11. 必须仅对本次任务选中的类别逐项检查全文。发现某类错误后仍须继续检查其他已选类别,但禁止检查和输出未选类别。\n"
"12. 若整段文本没有任何错误,只返回:###\n###,不要输出其它任何字符。输出完全部真实问题后立即结束,严禁重复已有记录或为了凑数量继续输出。\n"
"请参照以下示例完成校对:" "请参照以下示例完成校对:"
), ),
suffix=( suffix=(
"问题:{question}\n" "问题:{question}\n"
"输出答案前必须在内部完成以下扫描,但不要输出扫描过程:\n" "输出答案前只对任务要求中明确选中的类别完成内部扫描,不要输出扫描过程:\n"
"A. 逐字检查错别字和错别词语;\n" "选择基础校对时,检查错别字、错别词语、标点、标题文字格式、重复内容及错误词汇,但不得修改标题序号;\n"
"B. 逐行检查标题及结尾标点,同级标题格式必须一致;标题后直接接正文时检查是否应使用冒号;\n" "选择逻辑校对时,检查前后矛盾、时间冲突、因果关系、指代和上下文逻辑;\n"
"C. 按正文顺序检查各级标题序号是否重复、跳号、倒序或层级错误;\n" "选择合规性检查时,检查合规问题及敏感词汇。\n"
"D. 全文检索重复句子和连续文本,结合标题主题判断是否冗余;\n" "未选中的类别不得检查、修改和输出。输出完真实问题后立即结束,不得重复记录。\n"
"E. 继续检查逻辑与合规问题。\n"
"必须完成 A 至 E 后再输出全部问题,不得提前结束或限制问题数量。\n"
"每条原文必须从本次输入正文中逐字、连续、原样复制,严禁提前采用修改结果。建议只能写可直接替换的最终正文。\n" "每条原文必须从本次输入正文中逐字、连续、原样复制,严禁提前采用修改结果。建议只能写可直接替换的最终正文。\n"
"答案:" "答案:"
), ),
@ -147,30 +222,55 @@ def build_review_prompt(types: List[str], content: str, require: Optional[str])
if require is None: if require is None:
require = "" require = ""
require = require.strip() require = require.strip()
review_t = "现在需要你帮我完成以下文本校对任务:" selected_types = normalize_review_types(types)
for i , t in enumerate(types): excluded_types = [
review_type for review_type in REVIEW_TYPE_ORDER
if review_type not in selected_types
]
review_t = "现在需要你帮我完成以下文本校对任务:\n"
for i, t in enumerate(selected_types, start=1):
if t =="逻辑校对": if t =="逻辑校对":
review_t += f"{i} 对这段文本进行逻辑校对,主要是内容中词语或者其他内容本身是个合规词语,但在文本用词不当,不符合当前内容中逻辑,例如:#2026年终总结2025年对本公司的业务开战总结正确的内容应该为#2026年终总结2026年对本公司的业务开展总结\n" review_t += (
f"{i}. 逻辑校对:只检查正文内部有明确证据的前后矛盾、"
"时间先后冲突、因果关系错误、主体或指代冲突。"
"错别字、漏字、标点、编号、命令写法、术语纠正、措辞润色、"
"补充“共几部分”等信息完整性问题均不属于逻辑校对,禁止输出;"
"不能仅凭孤立词句推测逻辑错误。\n"
)
if t =="基础校对": if t =="基础校对":
review_t += f"{i}.1 进行错别字校对,错别字错误主要以文本用字不当为主,例如:星光店电,正确的应该为:星光点点。\n" review_t += f"{i}.1 进行错别字校对,错别字错误主要以文本用字不当为主,例如:星光店电,正确的应该为:星光点点。\n"
review_t += f"{i}.2 进行标点校对,主要以标点符号使用不当为主,例如:星光点点;万里无云,正确的应该为:星光点点,万里无云\n" review_t += f"{i}.2 进行标点校对,主要以标点符号使用不当为主,例如:星光点点;万里无云,正确的应该为:星光点点,万里无云\n"
review_t += f"{i}.3 进行格式规范校对\n" review_t += f"{i}.3 进行格式规范校对,但不得修改标题序号或图片编号\n"
review_t += f"{i}.4 进行重复内容校对,主要以原文中出现重复词语、句子或文本为主,例如:“今天今天心情相当不错,我很开心。我很开心。“,正确的内容应该为:今天心情相当不错,我很开心。\n" review_t += f"{i}.4 进行重复内容校对,主要以原文中出现重复词语、句子或文本为主,例如:“今天今天心情相当不错,我很开心。我很开心。“,正确的内容应该为:今天心情相当不错,我很开心。\n"
if t=="合规性检查": if t=="合规性检查":
review_t += f"{i} 对这段文本进行合规性检查\n" review_t += (
f"{i}. 合规性检查:只检查明文账号口令、访问密钥、令牌等凭据泄露,"
"以及正文中有明确依据的违法违规、隐私泄露或安全合规问题。"
"“保密资料”“军用设备”“未公开方案”“核心技术参数”等客观名称本身不构成错误,"
"不得为了降敏而改写事实。错别字、格式、逻辑和措辞问题均禁止输出。"
"只有额外要求明确提供了敏感词表时,才能输出“敏感词汇”,"
"且原文必须是词表中实际命中的原词。\n"
)
scope_text = (
f"本次选中的校对项:{''.join(selected_types)}\n"
f"本次禁止检查和输出的校对项:{''.join(excluded_types) if excluded_types else ''}\n"
"只允许输出所选校对项对应的错误类型;未选类别即使发现问题也禁止修改和输出。"
)
require_text = f"\n额外要求:{require}\n" if require else "" require_text = f"\n额外要求:{require}\n" if require else ""
query = f""" query = f"""
任务要求 任务要求
{review_t} {review_t}
校对范围
{scope_text}
{require_text} {require_text}
注意只输出结构化校对结果错误/原文/建议每条结果的错误原文建议必须连续三行三行之间不得有空行建议只写修改后的正确文本没有错误时只返回 ###\n###。 注意只输出结构化校对结果错误/原文/建议每条结果的错误原文建议必须连续三行三行之间不得有空行建议只写修改后的正确文本没有错误时只返回 ###\n###。
必须逐项检查错别字和错别词语标点标题格式与序号重复内容逻辑及合规重复内容必须结合标题和上下文判断删除局部重复内容时原文必须带唯一上下文建议必须返回删除后的完整上下文同一片段中修改范围重叠的多类错误必须合并为一条 必须仅完整检查上方明确选中的校对项禁止检查和输出未选类别重复内容必须结合标题和上下文判断删除局部重复内容时原文必须带唯一上下文建议必须返回删除后的完整上下文同一片段中修改范围重叠的多类错误必须合并为一条
待校对正文开始 待校对正文开始
{content} {content}
待校对正文结束 待校对正文结束
""".strip() """.strip()
combined_prompt = cotprompt(query) combined_prompt = cotprompt(query, selected_types)
return [ return [
ChatCompletionUserMessageParam(role="user", content=combined_prompt), ChatCompletionUserMessageParam(role="user", content=combined_prompt),
] ]
@ -274,16 +374,21 @@ def filter_positive_word_false_positives(
for item in items: for item in items:
original = str(item.get("original", "")) original = str(item.get("original", ""))
suggestion = str(item.get("suggestion", ""))
error = str(item.get("error", "")) error = str(item.get("error", ""))
has_positive_word = any(word in original for word in positive_words) has_positive_word = any(word in original for word in positive_words)
changes_positive_word = any(
word in original and word not in suggestion
for word in positive_words
)
is_typo_error = ( is_typo_error = (
"错别字" in error "错别字" in error
or "错别词" in error or "错别词" in error
or "错别词语" in error or "错别词语" in error
) )
if has_positive_word and is_typo_error: if changes_positive_word or (has_positive_word and is_typo_error):
continue continue
result.append(item) result.append(item)