|
@@ -0,0 +1,272 @@
|
|
|
|
|
+#!/usr/bin/env python
|
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
|
+
|
|
|
|
|
+import json
|
|
|
|
|
+from typing import List, Optional
|
|
|
|
|
+
|
|
|
|
|
+from pydantic import BaseModel, Field, ValidationError
|
|
|
|
|
+from langchain_core.prompts import ChatPromptTemplate
|
|
|
|
|
+from langchain_core.output_parsers import PydanticOutputParser, StrOutputParser
|
|
|
|
|
+from langchain_openai import ChatOpenAI
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 1) 定义结构 =====
|
|
|
|
|
+class PunctuationResult(BaseModel):
|
|
|
|
|
+ original_text: str = Field(..., description="审查的规范原文,与输入完全一致")
|
|
|
|
|
+ title_mark_status: bool = Field(..., description="书名号使用是否正确,true表示正确,false表示错误")
|
|
|
|
|
+ bracket_status: Optional[bool] = Field(..., description="括号使用是否正确,true表示正确,false表示错误,null表示没有编号")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+class PunctuationResults(BaseModel):
|
|
|
|
|
+ items: List[PunctuationResult]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 2) SYSTEM Prompt =====
|
|
|
|
|
+SYSTEM = """
|
|
|
|
|
+你是【标点符号规范性检查助手】。
|
|
|
|
|
+
|
|
|
|
|
+【任务】
|
|
|
|
|
+仅对已通过“成对出现”预检的文本,检查书名号和括号是否**包裹完整且位置正确**。
|
|
|
|
|
+(预检已经保证:
|
|
|
|
|
+ - 书名号《》至少各出现一次且数量相等
|
|
|
|
|
+ - 括号(/( )/)至少各出现一次且数量相等(中文、英文括号视为同类)
|
|
|
|
|
+ 因此你只需判断包裹范围是否正确、是否遗漏内容。)
|
|
|
|
|
+
|
|
|
|
|
+【判断标准】
|
|
|
|
|
+- title_mark_status:书名号需完全包裹规范名称,且不多包/漏包
|
|
|
|
|
+- bracket_status:括号需完全包裹规范编号,且不多包/漏包;编号可能是各种形式,如果文本中没有编号,设置为null
|
|
|
|
|
+
|
|
|
|
|
+【输出要求】
|
|
|
|
|
+- 为每个输入文本输出一个检查结果
|
|
|
|
|
+- 确保输出数量与输入一致
|
|
|
|
|
+- original_text 必须与输入完全一致
|
|
|
|
|
+- title_mark_status 必须是布尔值:true表示正确,false表示错误
|
|
|
|
|
+- bracket_status 必须是布尔值或null:true表示正确,false表示错误,null表示没有编号
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+HUMAN = """
|
|
|
|
|
+请检查以下文本中书名号和括号的**内容是否全部被包裹**,以及是否有编号。
|
|
|
|
|
+(所有文本已通过成对出现的预检,至少各有一对《》且数量相等。)
|
|
|
|
|
+
|
|
|
|
|
+【判断原则】
|
|
|
|
|
+- 仅检查包裹的**完整性**:书名号是否包裹了规范名称的全部内容;括号是否包裹了编号的全部内容
|
|
|
|
|
+- 中文括号()和英文括号()混用视为正常,不区分
|
|
|
|
|
+- 若内容在符号外遗漏,或符号包裹了多余内容,则判定为false
|
|
|
|
|
+- **重要**:如果文本中没有编号(完全没有任何()或()符号),则bracket_status设置为null
|
|
|
|
|
+
|
|
|
|
|
+【简单示例】
|
|
|
|
|
+示例1:《建筑抗震设》计规范 (GB 50011-2001)
|
|
|
|
|
+- 规范名称是"建筑抗震设计规范",但只有"建筑抗震设"被包裹,"计规范"在外 → title_mark_status=false
|
|
|
|
|
+- 编号被完整包裹 → bracket_status=true
|
|
|
|
|
+
|
|
|
|
|
+示例2:《建筑抗震设计规范》
|
|
|
|
|
+- 书名号包裹了完整的规范名称 → title_mark_status=true
|
|
|
|
|
+- 没有编号 → bracket_status=null
|
|
|
|
|
+
|
|
|
|
|
+示例3:《建筑抗震设计规范》(GB 50011-2001)
|
|
|
|
|
+- 书名号包裹了完整的规范名称 → title_mark_status=true
|
|
|
|
|
+- 英文括号包裹了完整的编号 → bracket_status=true(混用不算错)
|
|
|
|
|
+
|
|
|
|
|
+【待检查文本】
|
|
|
|
|
+{items}
|
|
|
|
|
+
|
|
|
|
|
+【输出格式要求】
|
|
|
|
|
+{format_instructions}
|
|
|
|
|
+/no_think
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+# ===== 3) Output Parser =====
|
|
|
|
|
+parser = PydanticOutputParser(pydantic_object=PunctuationResults)
|
|
|
|
|
+
|
|
|
|
|
+# ===== 4) Prompt =====
|
|
|
|
|
+prompt = ChatPromptTemplate.from_messages([
|
|
|
|
|
+ ("system", SYSTEM),
|
|
|
|
|
+ ("human", HUMAN)
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+# ===== 5) LLM =====
|
|
|
|
|
+llm = ChatOpenAI(
|
|
|
|
|
+ model="qwen3-30b",
|
|
|
|
|
+ base_url="http://192.168.91.253:8003/v1",
|
|
|
|
|
+ api_key="sk-123456",
|
|
|
|
|
+ temperature=0.7,
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 6) 提取第一个 JSON =====
|
|
|
|
|
+def extract_first_json(text: str) -> dict:
|
|
|
|
|
+ """从任意模型输出中提取第一个完整 JSON 对象 { ... }"""
|
|
|
|
|
+ start = text.find("{")
|
|
|
|
|
+ if start == -1:
|
|
|
|
|
+ raise ValueError("未找到 JSON 起始 '{'")
|
|
|
|
|
+
|
|
|
|
|
+ depth = 0
|
|
|
|
|
+ for i in range(start, len(text)):
|
|
|
|
|
+ ch = text[i]
|
|
|
|
|
+ if ch == "{":
|
|
|
|
|
+ depth += 1
|
|
|
|
|
+ elif ch == "}":
|
|
|
|
|
+ depth -= 1
|
|
|
|
|
+ if depth == 0:
|
|
|
|
|
+ return json.loads(text[start:i + 1])
|
|
|
|
|
+
|
|
|
|
|
+ raise ValueError("JSON 花括号未闭合")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 7) 核心方法 =====
|
|
|
|
|
+async def check_punctuation(items: List[str]) -> str:
|
|
|
|
|
+ """
|
|
|
|
|
+ 检查规范文本中的书名号和括号使用是否正确,先进行成对预检,再用LLM判断包裹完整性
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ items: 待检查的规范文本列表
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ 检查结果的JSON字符串,包含三个字段:
|
|
|
|
|
+ - original_text: 原文
|
|
|
|
|
+ - title_mark_status: 书名号使用是否正确(true/false)
|
|
|
|
|
+ - bracket_status: 括号使用是否正确(true/false/null,null表示没有编号)
|
|
|
|
|
+ """
|
|
|
|
|
+ # 1) 预检:是否存在且成对出现
|
|
|
|
|
+ pre_results = [] # 预填结果,若需LLM再补充
|
|
|
|
|
+ llm_inputs = [] # 需要LLM判定包裹完整性的文本
|
|
|
|
|
+
|
|
|
|
|
+ for text in items:
|
|
|
|
|
+ # 书名号成对判定
|
|
|
|
|
+ left_title = text.count("《")
|
|
|
|
|
+ right_title = text.count("》")
|
|
|
|
|
+ title_pair_ok = left_title == right_title and left_title > 0
|
|
|
|
|
+
|
|
|
|
|
+ # 括号成对判定(中英文括号混用视为同类)
|
|
|
|
|
+ left_br = text.count("(") + text.count("(")
|
|
|
|
|
+ right_br = text.count(")") + text.count(")")
|
|
|
|
|
+ bracket_pair_ok = left_br == right_br and left_br > 0
|
|
|
|
|
+
|
|
|
|
|
+ # 只有书名号和括号都存在时,才判断一一对应
|
|
|
|
|
+ # 情况1:都不存在 → 都为False
|
|
|
|
|
+ if left_title == 0 and left_br == 0:
|
|
|
|
|
+ pre_results.append({
|
|
|
|
|
+ "original_text": text,
|
|
|
|
|
+ "title_mark_status": False,
|
|
|
|
|
+ "bracket_status": None
|
|
|
|
|
+ })
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 情况2:只有书名号,没有括号 → bracket_status为None
|
|
|
|
|
+ if left_title > 0 and left_br == 0:
|
|
|
|
|
+ if title_pair_ok:
|
|
|
|
|
+ llm_inputs.append(text)
|
|
|
|
|
+ else:
|
|
|
|
|
+ pre_results.append({
|
|
|
|
|
+ "original_text": text,
|
|
|
|
|
+ "title_mark_status": False,
|
|
|
|
|
+ "bracket_status": None
|
|
|
|
|
+ })
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 情况3:只有括号,没有书名号 → title_mark_status为False
|
|
|
|
|
+ if left_title == 0 and left_br > 0:
|
|
|
|
|
+ pre_results.append({
|
|
|
|
|
+ "original_text": text,
|
|
|
|
|
+ "title_mark_status": False,
|
|
|
|
|
+ "bracket_status": bool(bracket_pair_ok)
|
|
|
|
|
+ })
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 情况4:两者都存在,判断一一对应
|
|
|
|
|
+ if left_title != left_br:
|
|
|
|
|
+ # 数量不对应,两个都为False
|
|
|
|
|
+ pre_results.append({
|
|
|
|
|
+ "original_text": text,
|
|
|
|
|
+ "title_mark_status": False,
|
|
|
|
|
+ "bracket_status": False
|
|
|
|
|
+ })
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 检查括号是否在书名号之后
|
|
|
|
|
+ bracket_after_title = True
|
|
|
|
|
+ if bracket_pair_ok and title_pair_ok:
|
|
|
|
|
+ # 找最后一个书名号的位置
|
|
|
|
|
+ last_title_pos = max(text.rfind("《"), text.rfind("》"))
|
|
|
|
|
+ # 找第一个括号的位置
|
|
|
|
|
+ first_bracket_pos = min(
|
|
|
|
|
+ text.find("(") if "(" in text else float('inf'),
|
|
|
|
|
+ text.find("(") if "(" in text else float('inf')
|
|
|
|
|
+ )
|
|
|
|
|
+ bracket_after_title = last_title_pos < first_bracket_pos
|
|
|
|
|
+
|
|
|
|
|
+ if not title_pair_ok or not bracket_pair_ok or not bracket_after_title:
|
|
|
|
|
+ # 预检失败或位置不正确,直接判定对应项为False,无需LLM
|
|
|
|
|
+ pre_results.append({
|
|
|
|
|
+ "original_text": text,
|
|
|
|
|
+ "title_mark_status": bool(title_pair_ok),
|
|
|
|
|
+ "bracket_status": bool(bracket_pair_ok and bracket_after_title)
|
|
|
|
|
+ })
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 成对且位置正确通过,交给LLM判断包裹是否完整和是否有编号
|
|
|
|
|
+ llm_inputs.append(text)
|
|
|
|
|
+
|
|
|
|
|
+ # 若无需要LLM的,直接返回预检结果
|
|
|
|
|
+ if not llm_inputs:
|
|
|
|
|
+ return json.dumps(pre_results, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+ chain = prompt | llm | StrOutputParser()
|
|
|
|
|
+ format_instructions = parser.get_format_instructions()
|
|
|
|
|
+
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "items": json.dumps(llm_inputs, ensure_ascii=False, indent=2),
|
|
|
|
|
+ "format_instructions": format_instructions
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ last_err = None
|
|
|
|
|
+
|
|
|
|
|
+ llm_result: List[dict] = []
|
|
|
|
|
+ for _ in range(2):
|
|
|
|
|
+ try:
|
|
|
|
|
+ raw = await chain.ainvoke(payload)
|
|
|
|
|
+ data = extract_first_json(raw)
|
|
|
|
|
+ findings = PunctuationResults.model_validate(data)
|
|
|
|
|
+ llm_result = [x.model_dump() for x in findings.items]
|
|
|
|
|
+ break
|
|
|
|
|
+ except (Exception, ValidationError, json.JSONDecodeError) as e:
|
|
|
|
|
+ last_err = e
|
|
|
|
|
+ print(f"[标点符号检查] 解析失败,重试中: {e}")
|
|
|
|
|
+
|
|
|
|
|
+ if last_err and not llm_result:
|
|
|
|
|
+ raise RuntimeError(f"标点符号检查失败:{last_err}") from last_err
|
|
|
|
|
+
|
|
|
|
|
+ # 合并预检与LLM结果,按原输入顺序输出
|
|
|
|
|
+ merged = []
|
|
|
|
|
+ llm_map = {item["original_text"]: item for item in llm_result}
|
|
|
|
|
+ for text in items:
|
|
|
|
|
+ # 先看预检是否已有
|
|
|
|
|
+ found = next((r for r in pre_results if r["original_text"] == text), None)
|
|
|
|
|
+ if found:
|
|
|
|
|
+ merged.append(found)
|
|
|
|
|
+ else:
|
|
|
|
|
+ merged.append(llm_map.get(text, {
|
|
|
|
|
+ "original_text": text,
|
|
|
|
|
+ "title_mark_status": False,
|
|
|
|
|
+ "bracket_status": None
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ return json.dumps(merged, ensure_ascii=False, indent=2)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ===== 8) 示例 =====
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ import asyncio
|
|
|
|
|
+
|
|
|
|
|
+ # 测试用例
|
|
|
|
|
+ test_items = [
|
|
|
|
|
+ "(4)《中华人民共和国突发事件应对法》【主席令〔2007〕第 69 号】;", # 正确
|
|
|
|
|
+ "《混》凝土结构设计规范(GB 50010-2010)", # 缺少书名号
|
|
|
|
|
+ "建筑施工组织设计规范GB/T 50502-2015", # 缺少括号
|
|
|
|
|
+ "《建筑抗震设计规范》(GB 50011)-2001", # 括号不成对
|
|
|
|
|
+ "《城市道路工程设计规范(CJJ 37-2012)", # 书名号不成对
|
|
|
|
|
+ "《公路工程技术标准》(JTG B01-2014)", # 正确
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+ result = asyncio.run(check_punctuation(test_items))
|
|
|
|
|
+ print("\n标点符号检查结果:")
|
|
|
|
|
+ print(result)
|