mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:11:59 +00:00
写作指引说清 [[see:]] 只放句末、码值写含义(A4、B4):写作规则后面加两条,依据标记渲染后不显示,写进句子中间会留下「以 为依据」这样的断句;涉及码值时写含义(「作废记录」),不写码值本身,数字核对不放宽。组装报告时认出拿得准的句中写法(「以 [[see:…]] 为」「根据、按照、基于、参见、详见 [[see:…]]」)记 inline_see,交回写作者重写,重写不好也不判失败;助手写报告节点的指引同样写明这两条
This commit is contained in:
@@ -219,7 +219,10 @@ NODE_REFERENCE = """\
|
||||
查过的数据源里真实存在的名字,表结构、查询、结果列里都没有的会被标成「可能是编造的名字」(受管模板的正式出具因此降档)
|
||||
- [[q:K1|逐字引文]]:知识库检索的原话,必须和命中片段里的字逐字一致(空白不计,至少 4 个字),改一个字就算
|
||||
没有出处;只是拿检索当依据时写 [[see:K1]]。要引原话,在 report 前面接 retrieve 节点
|
||||
- 句末的 [[see:m:指标id,Q1,K1]]:这句结论的依据,不显示
|
||||
- 句末的 [[see:m:指标id,Q1,K1]]:这句结论的依据,不显示,所以只能放在句末;instructions 里不要写「以 [[see:…]] 为依据」
|
||||
「根据 [[see:…]]」这样把它当成句子成分的示例,模型照着写会留下「以 为依据」这样的断句
|
||||
- 涉及码值(状态、类型这类用数字编码的取值)时,instructions 里让模型写它的含义(「作废记录」),不要写码值本身:
|
||||
码值是没有出处的数字,会被数字核对打回重写
|
||||
metrics_from 不写就取所有上游口径卡;on_violation 不写时探索运行是 flag(标出来、照常产出),正式运行是 fail;
|
||||
numbers 默认 strict(裸数字算违规)。claims 是结论句策略:off(默认,不管)/ require_citation(每句结论都要
|
||||
挂引用,没挂的计入缺口、出具降档)/ judge(在 require_citation 之外,再请另一个模型按每句挂的依据判断支不支持:
|
||||
|
||||
@@ -2631,7 +2631,8 @@ def compose_doc(
|
||||
"markdown": markdown, "source": source, "catalog": kept, "blocks": out_blocks,
|
||||
}
|
||||
checked = verify_doc(doc, catalog, allow_numbers=allow_numbers, cells_allowed=cells_allowed, loader=snaps)
|
||||
doc["stats"], doc["violations"] = checked["stats"], checked["violations"]
|
||||
# 句中的 [[see:]] 只在组装时查(交回写作者重写):出口复核看的是数字和出处,句子通不通不影响出具
|
||||
doc["stats"], doc["violations"] = checked["stats"], checked["violations"] + inline_see(source)
|
||||
return doc
|
||||
|
||||
|
||||
@@ -2999,6 +3000,38 @@ MARKER_RULES = """写作规则(系统会逐字核对):
|
||||
4. 日期、ISO 周(2026-W37)、「前 3 名」「第 2 季度」这类序号可以直接写。
|
||||
5. 目录里没有的指标不要编造引用;标着「没有值」的指标不要写进报告。"""
|
||||
|
||||
#: 跟在写作规则后面的两条(R2 走查 A4、B4):写作规则本身保持一期那份,这两条另起一段
|
||||
#: - [[see:]] 渲染后不显示:写进句子中间(「以 [[see:Q1]] 为依据看」)会留下「以 为依据看」这样的断句;
|
||||
#: - 码值(status = 9 里的 9)是没有出处的数字,会被数字核对打回重写:写它的含义,不写码值本身。
|
||||
SEE_AND_CODE_RULES = (
|
||||
"另外两条:\n"
|
||||
"- [[see:…]] 渲染后不显示,只能挂在句末(句号前后都行)。不要把它写进句子当成分:「以 [[see:Q1]] 为依据看,……」"
|
||||
"「根据 [[see:Q1]],……」渲染后会变成「以 为依据看」「根据 ,」这样的断句。先说结论,句末再写 [[see:Q1]]。\n"
|
||||
"- 涉及码值(状态、类型这类用数字编码的取值)时,写它的含义,比如「作废记录」「有效记录」,不要写码值本身"
|
||||
"(「状态为 9」「类型 1」):码值是没有出处的数字,系统会当成裸数字打回重写。"
|
||||
)
|
||||
|
||||
#: 句中的 [[see:]]:前面是介词、后面接着句子的写法(「以 [[see:Q1]] 为依据」「根据 [[see:Q1]],」「详见 [[see:Q1]]」)。
|
||||
#: 只认这几种拿得准的:「以」前面是「所、可、加……」的是另一个词(所以、可以),不算;「依据 [[see:…]]。」可能是
|
||||
#: 「这是结论的依据」,不算。认出来就交回写作者重写(report.REWRITE_ONLY:重写不好也不判失败)
|
||||
_SEE_MARK = r"\[\[" + _MARKER_GUARD + r"[ \t]*see[ \t]*:[^\[\]\n]*?\]\]"
|
||||
_INLINE_SEE = re.compile(
|
||||
r"(?<![所可加予得难足用是何之])以[ \t\u3000]*" + _SEE_MARK + r"[ \t\u3000]*为"
|
||||
r"|(?:根据|按照|基于|参见|详见)[ \t\u3000]*" + _SEE_MARK
|
||||
)
|
||||
INLINE_SEE_MESSAGE = _Reason(
|
||||
"依据标记写在了句子中间:依据不显示,正文会留下「以 为依据」这样的断句",
|
||||
"依据标记 [[see:…]] 写在了句子中间。它渲染后不显示,只能挂在句末:把这一句改成先说结论、句末再写 [[see:…]],"
|
||||
"不要写「以 [[see:Q1]] 为依据」「根据 [[see:Q1]]」「详见 [[see:Q1]]」",
|
||||
)
|
||||
|
||||
|
||||
def inline_see(source: str) -> list[dict[str, Any]]:
|
||||
"""原文里写在句子中间的 [[see:]](_INLINE_SEE),每处一条违规 inline_see。text 是那一截原文,没有 span:
|
||||
标记渲染后不在正文里,定位不到片段。"""
|
||||
return [_violation("inline_see", INLINE_SEE_MESSAGE, text=m.group(0)) for m in _INLINE_SEE.finditer(source or "")]
|
||||
|
||||
|
||||
#: 引用查询结果的写法。只跟着目录里的查询出现(catalog_prompt):写作规则本身保持一期那份,升级前
|
||||
#: 发起的运行跑到报告节点时提示一字不差;只有口径卡的目录也不该教写作者去引用不存在的 Q1
|
||||
CELL_RULES = (
|
||||
|
||||
@@ -48,6 +48,7 @@ from app.engine.evidence import (
|
||||
CLAIMS_RULE,
|
||||
ENTITY_KINDS,
|
||||
MARKER_RULES,
|
||||
SEE_AND_CODE_RULES,
|
||||
StreamRenderer,
|
||||
build_catalog,
|
||||
catalog_prompt,
|
||||
@@ -80,6 +81,8 @@ CLAIMS = CLAIMS_VALUES
|
||||
#: 只标注、不让写作者重写也不判失败的违规:可疑实体(受管的正式出具按缺口降档)、表结构快照不全时
|
||||
#: 核对不了的名字(出口只标注)
|
||||
SOFT = frozenset({"unknown_entity", "unverified_entity"})
|
||||
#: 交回写作者重写、重写不好也不判失败的违规:句中的 [[see:]](渲染后留下断句,数字和出处都没问题)
|
||||
REWRITE_ONLY = frozenset({"inline_see"})
|
||||
#: 重写一次就是多一次整篇的模型调用,写不对的模型多给几次也大多写不对
|
||||
MAX_REPAIRS = 3
|
||||
|
||||
@@ -304,6 +307,11 @@ def _blocking(violations: list[dict[str, Any]], numbers: str) -> list[dict[str,
|
||||
and not (numbers == "off" and v.get("code") == "uncited_number")]
|
||||
|
||||
|
||||
def _fatal(blocking: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""要判失败的违规:重写之后还在的、不属于 REWRITE_ONLY 的。"""
|
||||
return [v for v in blocking if v.get("code") not in REWRITE_ONLY]
|
||||
|
||||
|
||||
def _violation_key(violation: dict[str, Any]) -> tuple[str, str]:
|
||||
"""认一处违规:(code, 点名的字)。点名的字按 text、ref、message 的顺序取第一个有的。"""
|
||||
named = violation.get("text") or violation.get("ref") or violation.get("message") or ""
|
||||
@@ -348,7 +356,7 @@ def _messages(ctx: NodeContext, state: GraphState, catalog: dict[str, Any], cell
|
||||
system = ctx.render_str(ctx.cfg("system", ""), state)
|
||||
instructions = ctx.render_str(ctx.cfg("instructions", ""), state).strip() \
|
||||
or "根据下面的证据写一份简洁的报告,先总后分。"
|
||||
rules = MARKER_RULES
|
||||
rules = f"{MARKER_RULES}\n{SEE_AND_CODE_RULES}"
|
||||
if claims in ("require_citation", CLAIMS_JUDGE):
|
||||
# 裁判只看写作者挂的依据:要判,就更得每句都挂上
|
||||
rules = f"{rules}\n{CLAIMS_RULE}" + (f"\n{judging.JUDGE_RULE}" if claims == CLAIMS_JUDGE else "")
|
||||
@@ -451,7 +459,7 @@ async def run_report(state: GraphState, ctx: NodeContext) -> dict[str, Any]:
|
||||
# 探索运行默认按需裁判:点开哪句才判哪句,节点里不花这笔钱
|
||||
judgement = judging.on_demand(doc, on_unsupported=judge_cfg["on_unsupported"],
|
||||
rewrite_once=judge_cfg["rewrite_once"], writer_model=model_id)
|
||||
elif not (blocking and on_violation == "fail"):
|
||||
elif not (_fatal(blocking) and on_violation == "fail"):
|
||||
# 反正要判失败的报告不值得再花钱判(节点紧接着就报错)
|
||||
doc, response, blocking, judgement, usages = await _judge_inline(
|
||||
ctx, doc, catalog, judge_cfg, model_id=model_id, messages=[*messages, response],
|
||||
@@ -461,10 +469,11 @@ async def run_report(state: GraphState, ctx: NodeContext) -> dict[str, Any]:
|
||||
doc_artifact = await _store(doc, ctx)
|
||||
ctx.emit(EventType.REPORT_CHECKED, doc_artifact=doc_artifact, ok=not doc["violations"],
|
||||
stats=doc["stats"], violations=doc["violations"][:20], repairs=repairs,
|
||||
on_violation=on_violation, failed=bool(blocking) and on_violation == "fail",
|
||||
on_violation=on_violation, failed=bool(_fatal(blocking)) and on_violation == "fail",
|
||||
**({"claims": claims} if evidence_on else {}),
|
||||
**({"judge": judgement} if judgement is not None else {}))
|
||||
if blocking and on_violation == "fail":
|
||||
if (fatal := _fatal(blocking)) and on_violation == "fail":
|
||||
blocking = fatal
|
||||
head = ";".join(v["message"] for v in blocking[:5])
|
||||
more = f";…另有 {len(blocking) - 5} 处" if len(blocking) > 5 else ""
|
||||
raise NodeError(
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""写作指引:[[see:]] 只放句末;涉及码值时写含义、不写码值本身(R2 走查的 A4、B4)。
|
||||
|
||||
- A4:报告正文出现「以 为依据看」这样的断句。[[see:]] 渲染后不显示,写作者把它写进了句子中间
|
||||
(「以 [[see:Q1]] 为依据看,……」)。指引写明只放句末;组装文档时能稳妥认出的写法(「以 [[see:…]] 为」
|
||||
「根据 [[see:…]]」之类)报出来,交回写作者重写;重写不好也不判失败——数字和出处都没问题,只是句子不通。
|
||||
- B4:报告核对把码值 1、0、9 当成没有出处的数字,每次运行都强制重写一轮。数字核对不放宽(不可回退的约定),
|
||||
指引里写明:涉及码值时写它的含义(「作废记录」),不写码值本身。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.copilot import NODE_REFERENCE as COPILOT_PROMPT
|
||||
from app.engine.evidence import SEE_AND_CODE_RULES, build_catalog, compose_doc
|
||||
from app.engine.nodes.report import REWRITE_ONLY
|
||||
from tests.test_report_node import ( # noqa: F401 - 夹具按名字引入
|
||||
CLEAN,
|
||||
engine_up,
|
||||
events,
|
||||
finish,
|
||||
script,
|
||||
weekly,
|
||||
)
|
||||
|
||||
|
||||
def test_the_writer_is_told_to_put_see_at_the_end_and_write_code_meanings():
|
||||
assert "[[see:…]]" in SEE_AND_CODE_RULES and "句末" in SEE_AND_CODE_RULES
|
||||
assert "以 [[see:Q1]] 为依据" in SEE_AND_CODE_RULES # 点名不要这样写
|
||||
assert "码值" in SEE_AND_CODE_RULES and "含义" in SEE_AND_CODE_RULES and "作废记录" in SEE_AND_CODE_RULES
|
||||
|
||||
|
||||
def test_the_copilot_tells_report_instructions_the_same():
|
||||
"""助手写报告节点的指引(copilot 的系统提示)同样说清:see 只放句末,码值写含义。"""
|
||||
assert "只能放在句末" in COPILOT_PROMPT and "以 [[see:" not in COPILOT_PROMPT.replace("不要写「以 [[see:", "")
|
||||
assert "码值" in COPILOT_PROMPT and "含义" in COPILOT_PROMPT
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def catalog():
|
||||
card = {"kind": "metric_set", "caliber": "周报口径", "caliber_version": "v1",
|
||||
"metrics": [{"id": "gmv", "name": "销售额", "value": 45678.5, "unit": "元", "format": "thousands"}]}
|
||||
return build_catalog(nodes={"card": card}, ledger=[{"kind": "metric_set", "node_id": "card", "artifact": "c" * 64}])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [
|
||||
"以 [[see:m:gmv]] 为依据看,本周销售额 [[m:gmv]] 偏高。",
|
||||
"以[[see:m:gmv]]为依据,本周销售额 [[m:gmv]] 偏高。",
|
||||
"根据 [[see:m:gmv]],本周销售额 [[m:gmv]] 偏高。",
|
||||
"本周销售额 [[m:gmv]] 偏高,详见 [[see:m:gmv]]。",
|
||||
"基于 [[see:m:gmv]] 的结果,本周销售额 [[m:gmv]] 偏高。",
|
||||
])
|
||||
def test_see_used_as_part_of_the_sentence_is_flagged(catalog, text):
|
||||
doc = compose_doc(text, catalog)
|
||||
[v] = [v for v in doc["violations"] if v["code"] == "inline_see"]
|
||||
assert "[[see:" in v["text"] and "句末" in v["for_model"]
|
||||
assert "断句" in v["message"] and "[[see:" not in doc["markdown"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text", [
|
||||
"本周销售额 [[m:gmv]] 偏高。[[see:m:gmv]]",
|
||||
"本周销售额 [[m:gmv]] 偏高 [[see:m:gmv]]。",
|
||||
"本周销售额 [[m:gmv]] 偏高 [[see:m:gmv]],所以 [[see:m:gmv]] 下周要关注。", # 「所以」不是介词
|
||||
"这是结论的依据 [[see:m:gmv]]。",
|
||||
"可以 [[see:m:gmv]]。",
|
||||
])
|
||||
def test_see_at_the_end_of_a_clause_is_fine(catalog, text):
|
||||
doc = compose_doc(text, catalog)
|
||||
assert not [v for v in doc["violations"] if v["code"] == "inline_see"], doc["violations"]
|
||||
|
||||
|
||||
INLINE = "## 本周概览\n\n以 [[see:m:gmv]] 为依据看,本周([[i:week]])销售额 [[m:gmv]],环比 [[m:wow]];订单 [[m:orders]]。"
|
||||
|
||||
|
||||
async def test_inline_see_is_sent_back_for_one_rewrite(engine_up, monkeypatch):
|
||||
seen = script(monkeypatch, INLINE, CLEAN)
|
||||
row = await finish(weekly({"on_violation": "fail"}))
|
||||
assert row.status == "succeeded", row.error
|
||||
assert len(seen) == 2, "句中的依据应当交回重写一次"
|
||||
assert "只能挂在句末" in str(seen[1][-1].content)
|
||||
|
||||
|
||||
async def test_inline_see_that_survives_the_rewrite_does_not_fail_the_node(engine_up, monkeypatch):
|
||||
"""数字、出处都对得上,只是句子不通:重写还这样也不判失败,留在核对结果里。"""
|
||||
assert "inline_see" in REWRITE_ONLY
|
||||
seen = script(monkeypatch, INLINE)
|
||||
row = await finish(weekly({"on_violation": "fail"}))
|
||||
assert row.status == "succeeded", row.error
|
||||
assert len(seen) == 2
|
||||
[checked] = [e.data for e in await events(row.id, "report.checked")]
|
||||
assert checked["failed"] is False and [v["code"] for v in checked["violations"]] == ["inline_see"]
|
||||
Reference in New Issue
Block a user