Files
AgentLab/backend/app/core/events.py
T
ThearchyHelios 9727c21d8d 可点击证据第四期:结论句由模型裁判;只引单元格的问数据图能发成受管
一、结论句裁判

- 报告节点新增 claims: judge:写完、核对完之后,把结论句和它引用的证据摘录交给裁判模型,
  判为支持 / 部分支持 / 不支持 / 不是结论,附一句理由;判定只标注,不改写正文
- 送裁判的句子按固定规则预筛:带引用的、含方向词或因果词的送去判,短句、过渡句不送
- 证据摘录:指标写名称、值和原式;查询给 SQL 前 300 字和被引用的行,被引用的列一定给到;遮罩列一律不给;
  规则里写明「数字已经由系统核对过,不要复算」
- 一份报告合并成一次调用,超出句数就分批;结构化输出失败时退回 JSON 解析,再失败记为未裁判
- 裁判模型的优先级:节点上写的 > 设置里的「证据裁判模型」> Copilot 的模型;思考关闭;
  和写作模型相同时给警告(等于自己审自己)
- rewrite_once(默认关):把不支持的句子和理由交回写作者只改这几句,再裁一次;
  改写稿冒出原稿没有的问题就不采用,保留原稿和原来的判定

二、成本上限

- 每份报告的句数、金额、时长,探索运行每次点击的金额,全局每日金额,每一项都可以自定义,也都可以设成不限;
  默认 40 句、$0.05、30 秒,每次点击 $0.01,每天 $2
- 调用前先估算,超出上限按优先级截断:先判带引用的,再判含方向词、因果词的,其余记为未裁判并写明「已到上限」;
  不报错,也不让报告失败
- 每日花费按本地日期计数,持久化在设置表里;重跑同一次运行不重复计费;价格目录里没有的模型只计次数,并写明估不出金额

三、正式运行与探索运行

- 正式运行在报告节点内裁判,判定写进报告文档、随文档封存;不支持的按 on_unsupported(degrade / withhold)降档或不予出具,
  裁判没跑成记缺口,不能判完整出具
- 探索运行按需裁判:POST /api/runs/{id}/evidence/judge,点开哪句判哪句;判定作为 evidence.judged 事件追加在封存之后,
  标「封存后追加」,封存核对仍然一致;只读封存范围内的工件;同一句不重复判;运行在裁判期间被接着跑时不记录
- 正式运行调这个接口返回 409,写明「正式运行的裁判在节点内完成」

四、治理与自动修复

- G3 放开 judge:受管级别写 judge 必须写预算(judge.max_cost_usd,或显式写 null 表示不限)
- 发布前自动修复:缺预算时给选项(默认金额 / 不限),不替人选;
  从 require_citation 改成 judge 不算降低要求,judge 改回 off、withhold 放宽成 degrade 算降低要求
- Copilot 手册:受管模板可以写 judge 并带预算;问数据按需裁判,不替人打开结论句策略

五、界面

- 结论句句末放徽标:支持 ◆、部分支持 ◇、不支持 ! 整句浅底、未裁判 ?;不用表示「已核对」的绿色;n / N 也跳到不支持的句子
- 证据面板里「模型的解释」:带「模型判断 · 模型名 · 非确定」,封存后追加的另外标出;
  「请模型判断这句」只在探索运行、还没判过时出现,触顶时写明已到上限和怎么调
- 出具横幅多一段结论句计数;设置页新增「证据裁判」一组(裁判模型、各项上限、今日花费),设成不限时写明费用受什么约束;
  报告节点可以配 judge;运行轨迹里裁判相关的事件都有中文标题

六、门禁修正

- 引用模式下,图里没有口径卡(只引查询单元格)的,出具契约不再要求 metrics_from,受管门禁也不再要求 required:
  strict 加 cells 已经保证每个数都点得开、对不上就不予出具;有口径卡的照旧要求。
  以前问数据的图怎么都发不成受管,Copilot 补上契约也会被判「冒出新的问题」作废
2026-09-29 11:10:53 +08:00

92 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import time
import uuid
from enum import StrEnum
from typing import Any
from pydantic import BaseModel, Field
class EventType(StrEnum):
"""前端与后端之间的实时协议。画布高亮、时间线、token 流全部基于这套事件。"""
# run 级
RUN_STARTED = "run.started"
RUN_FINISHED = "run.finished"
RUN_FAILED = "run.failed"
RUN_CANCELLED = "run.cancelled"
RUN_INTERRUPTED = "run.interrupted" # 等待人工介入
RUN_RESUMED = "run.resumed"
# 节点级
NODE_STARTED = "node.started"
NODE_FINISHED = "node.finished"
NODE_FAILED = "node.failed"
NODE_SKIPPED = "node.skipped"
EDGE_TAKEN = "edge.taken" # 条件分支实际走了哪条边
# 模型级
LLM_START = "llm.start"
LLM_TOKEN = "llm.token"
# 思考的流式增量:只推给画布做实时显示,不落库
LLM_THINKING_DELTA = "llm.thinking.delta"
# 一次完整思考:调用结束时落一条,这才是进轨迹的事件
LLM_THINKING = "llm.thinking"
LLM_END = "llm.end"
# 工具 / 沙箱
TOOL_START = "tool.start"
TOOL_END = "tool.end"
TOOL_ERROR = "tool.error"
# 信任档是「始终允许 · 门控把关」的工具,门控模型对这一次调用的判定(tools/trust.py)。
# allow 之后是正常的 tool.start;escalate 之后是人工审批,协作团队里则是不执行
TOOL_GATED = "tool.gated"
# 知识检索。以前只发一条 info 日志,于是"这句结论依据的是哪份文档的哪一段"
# 在轨迹里查不到——而 SQL 取数那条路早就有工件快照可以下钻
RETRIEVE_END = "retrieve.end"
# 记忆读写。以前发的是 info 日志,而解码器丢弃所有 info——于是往长期记忆里
# 写东西这件事在界面上是隐形的。让 Copilot 主动记之后,这条不可接受
MEMORY_END = "memory.end"
SANDBOX_START = "sandbox.start"
SANDBOX_END = "sandbox.end"
# 多 agent 协作中的单个 agent 步骤
AGENT_STEP_START = "agent.step.start"
AGENT_STEP_END = "agent.step.end"
# 调度者的一次决策。它是一次结构化模型调用,常常占掉团队节点大半的时间;
# 以前前后都没有事件,界面上那几十秒是黑盒
AGENT_ROUTE_START = "agent.route.start"
AGENT_ROUTE_END = "agent.route.end"
# 人工介入
HUMAN_REQUESTED = "human.requested"
HUMAN_RESOLVED = "human.resolved"
# 出具与口径治理
ISSUANCE = "issuance" # 出具契约的判定结果(档位 / 缺数据 / 未回指数字)
CALIBER_UPGRADE = "caliber.upgrade" # 钉住的方法卡有新版本,按声明的策略处置
# 报告撰写节点的自查结果:报告文档工件 id、统计、违规清单。落在封存范围内,
# 证据接口靠它找到文档——不去翻不受封存保护的 artifacts 表
REPORT_CHECKED = "report.checked"
# 探索运行里按需裁判的结论句判定:点开哪句才判哪句,追加在封存之后(post_seal),是批注不是证据。
# 封存核对只覆盖 manifest_seq 之前的事件,追加它不影响 verify;封存的报告文档一个字都不改
EVIDENCE_JUDGED = "evidence.judged"
# 其他
LOG = "log"
USAGE = "usage" # token / 成本累计
class RunEventModel(BaseModel):
id: str = Field(default_factory=lambda: uuid.uuid4().hex)
run_id: str
seq: int = 0
type: EventType
node_id: str | None = None
ts: float = Field(default_factory=time.time)
data: dict[str, Any] = Field(default_factory=dict)
def to_wire(self) -> dict[str, Any]:
return self.model_dump(mode="json")