Commit dd919218 by ccran

feat: add optimize_review_prompt_flow

parent d9c25a8a
...@@ -6,16 +6,11 @@ ...@@ -6,16 +6,11 @@
# Keep Python source files # Keep Python source files
!**/*.py !**/*.py
!**/*.doc
!**/*.docx
!**/*.xlsx
!**/*.pdf
!**/*.xls
!workflow/** !workflow/**
!demo/** !demo/**
!skills/** !skills/**
!README.md !README.md
!data/*.xlsx
# Keep this file tracked # Keep this file tracked
!.gitignore !.gitignore
......
...@@ -16,31 +16,40 @@ from utils.http_util import upload_file, fastgpt_openai_chat, download_file ...@@ -16,31 +16,40 @@ from utils.http_util import upload_file, fastgpt_openai_chat, download_file
use_lufa = False use_lufa = False
batch_size = 5 batch_size = 5
if not use_lufa:
SUFFIX = "_麓发迁移" def get_params():
batch_input_dir_path = "jp-input" output_suffix = f"{time.strftime('%Y%m%d-%H%M%S')}-{time.time_ns() % 1_000_000:06d}"
batch_output_dir_path = f"/data/home/htsc/jp-contract/data/benchmark/results/jp-output-simple" if not use_lufa:
# 金盘fastgpt接口 return {
url = "http://172.21.107.45:3002/api/v1/chat/completions" "suffix": "_麓发迁移",
# 金盘迁移麓发合同审查测试token "batch_size": batch_size,
# token = "fastgpt-vykT6qs07g7hR4tL2MNJE6DdNCIxaQjEu3Cxw9nuTBFg8MAG3CkByvnXKxSNEyMK7" "batch_input_dir_path": "jp-input",
token = 'fastgpt-pYh0DgMVPDh9DptmznbSz7fRAqS41Z7gUWWIPM0112APpMlbb8mc1iztJi' "batch_output_dir_path": f"/data/home/htsc/jp-contract/data/benchmark/results/jp-output-{output_suffix}",
# 人机交互测试(测试环境) # 金盘fastgpt接口
# token = 'fastgpt-p189K5zoTX5wjp0dBybFCwsbWm3juIwlJxt2wTGyiaOWOANI5Y10pKEZzyt' "url": "http://172.21.107.45:3002/api/v1/chat/completions",
# 人机交互测试(生产环境) # 金盘迁移麓发合同审查测试token
# token = "fastgpt-ry4jIjgNwmNgufMr5jR0ncvJVmSS4GZl4bx2ItsNPoncdQzW9Na3IP1Xrankr" # "token": "fastgpt-vykT6qs07g7hR4tL2MNJE6DdNCIxaQjEu3Cxw9nuTBFg8MAG3CkByvnXKxSNEyMK7",
# 提取后审查测试 "token": "fastgpt-pYh0DgMVPDh9DptmznbSz7fRAqS41Z7gUWWIPM0112APpMlbb8mc1iztJi",
# token = 'fastgpt-n74gGX5ZqLT6o1ysMBSGUTjIciswYOWDRfQ75krMkE5gDVDkpzsbz8u' # 人机交互测试(测试环境)
else: # "token": "fastgpt-p189K5zoTX5wjp0dBybFCwsbWm3juIwlJxt2wTGyiaOWOANI5Y10pKEZzyt",
SUFFIX = "_麓发" # 人机交互测试(生产环境)
batch_input_dir_path = "4.24测财务合同审核" # "token": "fastgpt-ry4jIjgNwmNgufMr5jR0ncvJVmSS4GZl4bx2ItsNPoncdQzW9Na3IP1Xrankr",
batch_output_dir_path = "4.24测财务合同审核-batch" # 提取后审查测试
# 麓发fastgpt接口 # "token": "fastgpt-n74gGX5ZqLT6o1ysMBSGUTjIciswYOWDRfQ75krMkE5gDVDkpzsbz8u",
url = "http://192.168.252.71:18089/api/v1/chat/completions" }
# 麓发合同审查生产token
# token = "fastgpt-ek3Z6PxI6sXgYc0jxzZ5bVGqrxwM6aVyfSmA6JVErJYBMr2KmYxrHwEUOIMSYz" return {
# 麓发合同审查生产token-标准化 "suffix": "_麓发",
token = "fastgpt-mg5tQUgreJeF7peoOr5zqP0NR4EIrfS2bEVXge6FUL94Suu1TvEMR1sGNRSiV" "batch_size": batch_size,
"batch_input_dir_path": "4.24测财务合同审核",
"batch_output_dir_path": f"4.24测财务合同审核-batch-{output_suffix}",
# 麓发fastgpt接口
"url": "http://192.168.252.71:18089/api/v1/chat/completions",
# 麓发合同审查生产token
# "token": "fastgpt-ek3Z6PxI6sXgYc0jxzZ5bVGqrxwM6aVyfSmA6JVErJYBMr2KmYxrHwEUOIMSYz",
# 麓发合同审查生产token-标准化
"token": "fastgpt-mg5tQUgreJeF7peoOr5zqP0NR4EIrfS2bEVXge6FUL94Suu1TvEMR1sGNRSiV",
}
def extract_url(text): def extract_url(text):
...@@ -59,7 +68,7 @@ def extract_url(text): ...@@ -59,7 +68,7 @@ def extract_url(text):
def process_single_file( def process_single_file(
file, batch_input_dir_path, batch_output_dir_path, counter, start_file file, params, counter, start_file
): ):
""" """
单文件处理逻辑,可被线程池并发调用 单文件处理逻辑,可被线程池并发调用
...@@ -68,14 +77,20 @@ def process_single_file( ...@@ -68,14 +77,20 @@ def process_single_file(
if start_file > counter: if start_file > counter:
return return
batch_input_dir_path = params["batch_input_dir_path"]
batch_output_dir_path = params["batch_output_dir_path"]
suffix = params["suffix"]
url = params["url"]
token = params["token"]
# 提取文件前缀 # 提取文件前缀
file_name = file[: file.rfind(".")] file_name = file[: file.rfind(".")]
ext_name = file[file.rfind(".") :] ext_name = file[file.rfind(".") :]
# 源目标处理 # 源目标处理
original_file = f"{batch_input_dir_path}/{file}" original_file = f"{batch_input_dir_path}/{file}"
des_check_file = f"{batch_output_dir_path}/{file_name}.md" des_check_file = f"{batch_output_dir_path}/{file_name}.md"
des_excel_file = f"{batch_output_dir_path}/{file_name}{SUFFIX}.xlsx" des_excel_file = f"{batch_output_dir_path}/{file_name}{suffix}.xlsx"
des_doc_file = f"{batch_output_dir_path}/{file_name}{SUFFIX}{ext_name}" des_doc_file = f"{batch_output_dir_path}/{file_name}{suffix}{ext_name}"
try: try:
# 处理原文件 # 处理原文件
...@@ -117,7 +132,11 @@ def process_single_file( ...@@ -117,7 +132,11 @@ def process_single_file(
logger.error(traceback.print_exc()) logger.error(traceback.print_exc())
def execute_batch(max_workers: int = 4): def execute_batch(params=None):
params = params or get_params()
max_workers = params["batch_size"]
batch_input_dir_path = params["batch_input_dir_path"]
batch_output_dir_path = params["batch_output_dir_path"]
start_file = 1 start_file = 1
dirs = os.listdir(batch_input_dir_path) dirs = os.listdir(batch_input_dir_path)
os.makedirs(batch_output_dir_path, exist_ok=True) os.makedirs(batch_output_dir_path, exist_ok=True)
...@@ -127,8 +146,7 @@ def execute_batch(max_workers: int = 4): ...@@ -127,8 +146,7 @@ def execute_batch(max_workers: int = 4):
executor.submit( executor.submit(
process_single_file, process_single_file,
file, file,
batch_input_dir_path, params,
batch_output_dir_path,
counter, counter,
start_file, start_file,
) )
...@@ -142,6 +160,7 @@ def execute_batch(max_workers: int = 4): ...@@ -142,6 +160,7 @@ def execute_batch(max_workers: int = 4):
if __name__ == "__main__": if __name__ == "__main__":
import os import os
execute_batch(batch_size) params = get_params()
execute_batch(params)
print("all done!") print("all done!")
print("文件保存在: ", os.path.abspath(batch_output_dir_path)) print("文件保存在: ", os.path.abspath(params["batch_output_dir_path"]))
...@@ -8,7 +8,7 @@ from typing import Iterable ...@@ -8,7 +8,7 @@ from typing import Iterable
import pandas as pd import pandas as pd
from spire.doc import Document from spire.doc import Document
from compare_annotation import compare_with_log from compare_annotation import compare
# Map raw comment authors to unified review item names. # Map raw comment authors to unified review item names.
COMMENT_AUTHOR_MAPPING: dict[str, str] = { COMMENT_AUTHOR_MAPPING: dict[str, str] = {
...@@ -86,8 +86,8 @@ def extract_annotaion( ...@@ -86,8 +86,8 @@ def extract_annotaion(
def compare_annotaion(val_dir: Path, answer_dir: Path) -> None: def compare_annotaion(val_dir: Path, answer_dir: Path) -> None:
"""Run benchmark comparison on extracted annotations.""" """Run benchmark comparison on extracted annotations."""
log_path = compare_with_log(val_dir=val_dir, answer_dir=answer_dir) output_excel = compare(val_dir=val_dir, answer_dir=answer_dir)
print(f"Compare log written to: {log_path}") print(f"Compare result written to: {output_excel}")
def _strip_suffix_once(stem: str, suffixes: Iterable[str]) -> str: def _strip_suffix_once(stem: str, suffixes: Iterable[str]) -> str:
...@@ -121,7 +121,7 @@ def _parse_args() -> argparse.Namespace: ...@@ -121,7 +121,7 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument( parser.add_argument(
"--datasets-dir", "--datasets-dir",
type=Path, type=Path,
default=base / "results" / "jp-output-lufa-20260511-101828", default=base / "results" / "jp-output-lufa-20260514-182859",
help="Directory containing Word files with annotations.", help="Directory containing Word files with annotations.",
) )
parser.add_argument( parser.add_argument(
...@@ -143,7 +143,7 @@ if __name__ == "__main__": ...@@ -143,7 +143,7 @@ if __name__ == "__main__":
datasets_dir=args.datasets_dir, datasets_dir=args.datasets_dir,
answer_dir=base / "审查答案", answer_dir=base / "审查答案",
val_dir=args.datasets_dir.with_name( val_dir=args.datasets_dir.with_name(
f"{args.datasets_dir.name}-extract-comment" f"{args.datasets_dir.name}-测评结果"
), ),
strip_suffixes=args.strip_suffixes, strip_suffixes=args.strip_suffixes,
) )
No preview for this file type
...@@ -346,4 +346,4 @@ MERGE_SYSTEM_PROMPT = """ ...@@ -346,4 +346,4 @@ MERGE_SYSTEM_PROMPT = """
"suggestion": "提炼合并后的建议" "suggestion": "提炼合并后的建议"
} }
``` ```
""" """
\ No newline at end of file
---
name: review-prompt-optimization-skill
description: 审查提示词/规则优化编排 Skill。通过 batch 批处理、benchmark eval、测评 Excel 归因,循环更新 data/rules.xlsx 中的审查规则。
---
# Review Prompt Optimization Skill
## 定位
`review-prompt-optimization-skill` 用于编排合同审查提示词和规则库优化闭环。
本 Skill 负责定义流程、判读方法、规则更新约束和循环停止条件。使用时,LLM 需要结合可用终端、Excel 工具和代码编辑能力执行流程;不要只给建议。
默认优化目标文件:
- 规则库:`data/rules.xlsx`
- 批处理脚本:`data/batch/batch.py`
- 测评脚本:`data/benchmark/eval.py`
- Excel 工具:`skills/doc-excel-skill/scripts/excel_tool.py`
- 编排辅助脚本:`data/optimize_review_prompt_flow.py`
## 标准闭环
1. 启动批处理
- 优先进入或使用 tmux session:`tmux attach -t batch`
- 在该 session 中运行:`python3 data/batch/batch.py`
- 也可直接使用 `data/optimize_review_prompt_flow.py` 串联执行。
2. 等待 batch 完成
- 每 5 分钟检查一次 tmux 输出。
- 完成标志是 `batch.py` 输出批处理结果路径,格式通常为:`文件保存在: /abs/path/to/output`
- 记录该路径为 `batch_output_dir`
3. 运行测评
- 执行:
```bash
python3 data/benchmark/eval.py --datasets-dir "$batch_output_dir"
```
- `eval.py` 会先抽取批注,再对比 `data/benchmark/审查答案`,并输出测评 Excel。
- 测评 Excel 通常位于:`data/benchmark/results/{batch输出目录名}-测评结果.xlsx`
4. 读取测评 Excel
- 先读取全部 sheet:
```bash
python3 skills/doc-excel-skill/scripts/excel_tool.py list-sheets "$eval_excel"
```
- 跳过汇总 sheet `对比结果`,遍历其余每个审查项 sheet。
- 每个审查项 sheet 有两类错误:
- 第一列 `审查不合格但是没有查出来的`:漏检,应增强召回。
- 第二列 `审查合格但是误判为不合格的`:误报,应收紧边界。
5. 更新规则库
- 更新目标是 `data/rules.xlsx` 中与 sheet 名或列 `审查项` 对应的规则行。
- 更新前必须备份,例如:
```bash
cp data/rules.xlsx "data/rules.xlsx.bak.$(date +%Y%m%d-%H%M%S).xlsx"
```
- 对漏检样例:
-`审查规则` 中补充该类风险的明确判定条件。
- 必要时补充 `触发词`,提高路由和规则命中。
- 如果样例反映的是规则缺项,而非单条规则表述不足,可新增或拆分审查项。
- 对误报样例:
-`审查规则` 中补充排除条件、合格边界、适用前提或反例。
- 必要时在 `建议模板` 中约束“满足某条件时无需修改”。
- 不要为了消除误报而删除应检风险的核心判定。
- 对同一审查项同时存在漏检和误报时:
- 先抽象共同原因,再修改规则。
- 避免简单堆砌样例,优先写可泛化的规则边界。
6. 循环验证
- 保存 `data/rules.xlsx` 后,再回到步骤 1 执行下一轮 batch/eval。
- 每轮记录:
- batch 输出目录
- eval Excel 路径
- 修改过的 sheet/审查项
- 漏检数量、误报数量、F1、查全率、查准率变化
- 当总体 F1 或目标审查项指标不再明显提升,或剩余错误已无法通过规则库优化稳定解决时停止。
## 辅助脚本
可使用辅助脚本完成“batch + eval + 规则优化 + 每轮结果落 Excel”:
```bash
python data/optimize_review_prompt_flow.py --rounds 1 --f1-threshold 0.9
```
默认行为:
- 调用 `data/batch/batch.py` 生成批处理输出目录。
- 调用 `data/benchmark/eval.py` 生成测评 Excel。
- 使用 `utils/excel_tool.py` 读取 `data/rules.xlsx` 和测评 Excel。
- 对 F1 低于阈值且存在漏检/误报的审查项,调用 `utils/openai_util.py` 批量优化规则。
- 写回 `data/rules.xlsx` 前自动备份。
- 每轮结果保存到 `data/benchmark/results/prompt_optimization_rounds.xlsx` 的新 sheet。
如果 batch 已经跑完,可跳过 tmux:
```bash
python data/optimize_review_prompt_flow.py \
--batch-output /abs/path/to/batch-output
```
常用参数:
- `--rounds 3`
- `--f1-threshold 0.9`
- `--example-limit 20`
- `--max-workers 10`
- `--dry-run`
## Excel 判读规则
### `对比结果`
用于确定优化优先级:
- 优先处理 `大模型未匹配上的不合格项(C-B)` 高的审查项,提高召回。
- 其次处理 `大模型其他不合格项``误报率(D/B+D)` 高的审查项,提高准确性。
- 同等情况下优先处理合同风险更高、出现频率更高的审查项。
### 审查项明细 sheet
每个明细 sheet 的 sheet 名即审查项名称。
- 第一列非空样例是“应该判不合格,但没有输出”的证据文本。
- 第二列非空样例是“实际合格,但被模型判不合格”的证据文本。
- 修改规则时,必须把样例上升为抽象规则,不要只复制样例原文。
## 更新 `data/rules.xlsx` 的约束
- 保留原有 sheet、列名、ID 和风险等级,除非确实需要新增规则。
- 优先更新与测评 sheet 同名或语义对应的 `审查项` 行。
- 只能改与本轮错误归因直接相关的规则,不做无关重构。
- 修改应同时兼顾召回和准确性,避免单向拉高导致另一类错误恶化。
- 每次修改后都要保存 Excel,并在最终回复中说明改了哪些审查项。
- 如果工具无法安全写入 Excel,应先导出修改建议 JSON/Markdown,不得破坏原文件。
## 推荐规则改写格式
`审查规则` 中优先使用以下结构:
```text
检查……。不合格情形包括:1)……;2)……。
合格/不判定为不合格的情形包括:1)……;2)……。
仅当合同原文明确体现……时输出不合格;不得因……直接推断为不合格。
```
`触发词` 中使用分号分隔:
```text
付款;支付;验收;尾款;发票
```
`建议模板` 中写可执行建议:
```text
如存在该风险,建议补充……;若合同已明确……则无需修改。
```
## 输出要求
每轮优化结束后,LLM 应输出:
- batch 输出目录
- eval Excel 路径
- 本轮修改的 `data/rules.xlsx` 备份路径
- 修改过的审查项列表
- 每个审查项的漏检/误报归因摘要
- 是否建议继续下一轮
## 注意事项
- 不要把 `对比结果` 当成规则明细 sheet。
- 不要把“审查合格但是误判为不合格的”样例加入不合格判定条件;它们用于写排除条件。
- 不要因单个样例过拟合规则,应提炼可泛化的合同审查边界。
- 批处理和测评耗时较长,轮询期间保持 tmux session 不被关闭。
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment