Skip to main content
Glama

groundtruth-mcp

ci pypi python license

你的编码智能体可以读取仓库中的每个文件,却仍然是在猜测。 它将你项目自身的检查、回放、模拟和查询转化为 MCP 工具,从而让智能体观察其编辑带来的后果,而不是预测它们。

中文文档 · 采用指南 · 架构 · 为什么使用固定种子


问题所在

正在编辑结构化配置——工作流图、规则文件、状态机、流水线定义——的智能体,所使用的上下文是错误的类型。它可以读取 schema,却无法读取程序运行时实际发生的事情。

于是它只能推断。它修改了一个重试限制,然后告诉你这个改动是安全的,因为“安全”是在一个看起来合理的 diff 下最有可能的下一个 token。没有人真正运行过任何东西。它所违反的约束,存在于三个文件之外的一个不变量里,或者存在于自上次策略调优以来再没人采样过的分布中。

解决办法不是更好的提示词,而是给智能体一些可以观察的东西。

Related MCP server: MCP Software-Engineering RL Environment

它的功能

flowchart LR
    E[Agent edits a config] --> L[lint]
    L -->|DANGLING_TRANSITION at states 1.transitions 0.to| E
    E --> R[replay seed=7]
    R -->|the 5 steps that actually ran| E
    E --> S[simulate 2000 seeds]
    S -->|88.3% success · p95 2566ms · PASS| E
    S --> G["CI: groundtruth simulate --gate"]
    G -->|same config, same thresholds| S

五个工具,由你编写的四个小函数构建而成:

工具

回答的问题

让它有用的属性

lint

这份配置自洽吗?

每个问题都带有需要编辑的确切路径

replay

当我运行这一个时会发生什么?

(config, seed) 的纯函数——在任何地方都可复现

simulate

我的改动总体上更好还是更糟?

带种子的批量、分布、阈值、通过/失败

query

数据中实际有什么?

由数据库强制只读,而不是靠正则表达式

describe_data

存在哪些表?

这样就不必去猜 schema

同样的能力也可以作为 CLI 运行,因此 groundtruth simulate --gate 就是一个读取智能体所优化的同一套阈值的合并门禁。它们不会漂移,因为只有一份副本。

六十秒上手

pip install "groundtruth-mcp[mcp]"

git clone https://github.com/ZhenGtai123/groundtruth-mcp && cd groundtruth-mcp
groundtruth --config examples/checkout-flow/groundtruth.toml lint broken_checkout

内置示例是一个由配置驱动的结账流程:四个页面、一个不稳定的支付网关、一套重试策略,以及会流失的顾客。broken_checkout.json 包含了智能体在编辑它无法运行的配置时实际会犯的错误。

broken_checkout: BLOCKED  errors=6 warnings=1 infos=0
source: flows\broken_checkout.json

-- ERRORS — these block (6) --
[DANGLING_TRANSITION] states[1].transitions[0].to  'payment_methd' does not name any states.id
    fix: point it at an existing state id, or delete the transition
[DEAD_END] states[6]  'review_hold' has no outgoing edge and is not marked terminal — a run that arrives here stops with no result
    fix: give it a transition, or mark it kind = "terminal" with an outcome
[DUPLICATE_STATE] states[2]  duplicate id='shipping' (first declared at states[1])
    fix: rename one of them; the engine silently uses the first and ignores the rest
[RATE_OUT_OF_RANGE] policy.gateway_failure_rate  1.4 is above the maximum 1.0
    fix: this is a probability, not a percentage — 0.18, not 18
[RETRY_BUDGET_TOO_THIN] policy.max_retries  140% gateway failure with 1 retries leaves 196.0% of checkouts failing on payment alone (budget: 2.0%)
    fix: raise max_retries, or lower gateway_failure_rate if the gateway improved
[UNKNOWN_STATE_KIND] states[3].kind  'stage' is not one of ['step', 'gateway', 'retry', 'terminal']
    fix: the engine only knows these four kinds; anything else is treated as a plain step

-- WARNINGS (1) --
[UNREACHABLE_STATE] states[4]  'gift_wrap' cannot be reached from 'cart_review'
    fix: no path from start reaches this state — delete it, or wire it in

其中六个来自规则文件。RETRY_BUDGET_TOO_THIN 来自八行 Python 代码,因为“这个重试预算是否达到产品的失败目标”是算术问题,而不是 schema。

现在看一次运行:

groundtruth --config examples/checkout-flow/groundtruth.toml replay standard_checkout --seed 3
standard_checkout  seed=3  outcome=success  steps=7  fingerprint=52b66a2024a61b5d
metrics: latency_ms=2506  payment_attempts=2  steps=7

-- TRACE --
  0. cart_review --always-->
  1. shipping --always-->
  2. payment_method --always-->
  3. authorize --failure-->  # attempt 1 declined
  4. retry_decision --retries_left-->  # 0 retry(s) used of 2
  5. authorize --success-->  # attempt 2 authorized
  6. confirmed  # terminal: success

种子 3 总会产生那七步——在你的机器上、在 CI 中、明年也是如此。这就是它值得一读的原因。

再看两千次:

groundtruth --config examples/checkout-flow/groundtruth.toml \
  simulate standard_checkout --runs 2000 --seed 0 --gate --check-determinism
standard_checkout: PASS  runs=2000  base_seed=0  fingerprint=449e16b50c8184c0

-- OUTCOMES --
  success: 1767 (88.3%)
  abandoned: 227 (11.3%)
  payment_failed: 6 (0.3%)

-- METRICS (mean / p50 / p95 / max) --
  latency_ms: 1587.75 / 1553 / 2566 / 3626
  payment_attempts: 1.06 / 1 / 2 / 3
  steps: 5.12 / 5 / 7 / 10

-- THRESHOLDS --
  PASS  rate:success = 0.8835  expected >= 0.8  (below this, the flow is losing customers faster than the business case allows)
  PASS  rate:stuck = 0  expected <= 0  (a run with nowhere to go is always a config bug, never bad luck)
  PASS  p95:latency_ms = 2566  expected <= 4000  (95th-percentile checkout wall time, retries included)
  PASS  mean:payment_attempts = 1.0585  expected <= 1.6  (rising attempts mean the gateway is degrading or the retry policy is too eager)

note: determinism: 20 seeds re-ran identically

真正体现价值的部分

调高一个数字——shipping.abandon_chance 从 0.05 改为 0.28——这种改动看起来像一次产品调整,并能通过评审:

$ groundtruth lint standard_checkout
standard_checkout: OK  errors=0 warnings=0 infos=0     # exit 0

$ groundtruth simulate standard_checkout --runs 2000 --seed 0 --gate
standard_checkout: FAIL  runs=2000  base_seed=0  fingerprint=5a7c0d9feed5adca

-- OUTCOMES --
  success: 1336 (66.8%)
  abandoned: 660 (33.0%)

-- THRESHOLDS --
  FAIL  rate:success = 0.668  expected >= 0.8
  PASS  rate:stuck = 0  expected <= 0
  PASS  p95:latency_ms = 2549  expected <= 4000
  PASS  mean:payment_attempts = 0.795  expected <= 1.6
                                                       # exit 1

结构上完美无缺,却少了 21 个百分点的转化率。任何 schema、类型系统或代码评审都抓不到这个问题;而一个带声明区间的种子批次在四秒内就能在拉取请求上发现它,甚至早于人类阅读 diff。

反过来也同样有效。express_checkout 发布的成功率高于标准流程——91.0%——但它其实是更差的配置:其支付失败率为 3.9%,而标准流程只有 0.3%,这个数字隐藏在一个看起来不错的总览数字之下。聚合指标发现不了它;手写的校验器却直截了当地指出了这一点:

[RETRY_BUDGET_TOO_THIN] policy.max_retries  18% gateway failure with 1 retries
leaves 3.2% of checkouts failing on payment alone (budget: 2.0%)

这两层并不能互相替代,所以才需要两层。

采用它

一个模块,一个配置文件。examples/checkout-flow/groundtruth_app.py 就是完整的模板——包括注释在内大约一百行。

from groundtruth_mcp import Context, Issue, Loaded, Toolkit, Trace

kit = Toolkit(name="my-project", subject_noun="pipeline")

@kit.loader
def load(name: str):
    path = CONFIG_DIR / f"{name}.yaml"
    if not path.is_file():
        return None                        # → "no pipeline named X; available: ..."
    return Loaded(subject=parse(path), source=str(path))

@kit.validator
def check(pipeline, ctx: Context) -> list[Issue]:
    ...                                    # the checks a rule file can't express

@kit.runner
def run_once(pipeline, seed: int, ctx: Context) -> Trace:
    ...                                    # one run, pure in (pipeline, seed)

仅 @kit.runner 一个装饰器就能同时提供 replay 和 simulate——库会为每个种子运行一次并保留结果。其余一切(种子批处理、聚合、百分位数、阈值门禁、输出预算、错误措辞、MCP 接口)都由包本身提供。

# groundtruth.toml
[project]
toolkit = "groundtruth_app:kit"

[lint]
rules = "rules.toml"

[[thresholds]]
metric = "rate:success"
min = 0.80
note = "why this number, for whoever has to change it"

然后 groundtruth doctor 告诉你哪些已连接,groundtruth serve 把工具交给智能体,groundtruth simulate --gate 阻止合并。完整的分领域示例逐步讲解见:docs/ADOPTION.md。

免费获得的规则

结构检查是声明出来的,而不是手写的。十二种类型,每一种都覆盖了结构化配置实际腐化的一种方式:

类型

捕获的问题

关键字段

required_fields

只写了一半的条目

select, fields

unique_key

引擎静默遮蔽的重复 ID

select, key

enum

你的引擎无法处理的值

select, values

type

本应是数字却放了字符串

select, expect

range

在概率字段中出现了 1.4

select, min, max

pattern

违反命名约定的 ID

select, regex

not_empty

在需要一个条目的地方出现了空列表

select

ref_exists

引用了已被重命名的对象

select, collection, key

reachable

没有从起点出发的路径能到达的节点

collection, key, edges, start

no_dead_end

没有出口的非终止节点

collection, key, edges, terminal_field

no_self_loop

转换到自身的节点

collection, key, edges

no_cycle

没有出口的环(对于有意的环,可以用 allow 列表放行)

collection, key, edges

选择器是一种刻意保持精简的路径语言——states[].transitions[].to——每次匹配都会报告找到该匹配的具体路径,这正是 states[3].transitions[1].to 能够实现的原因,而不是仅仅报告“某个转换无效”。

每条规则都有可选的 code、severity 和 hint。hint 是智能体将据此行动的句子,所以请用祈使句来写。

只读就是只读

query 只执行一条 SELECT。有两层机制来保证这一点,而它们并不平等。

关键词扫描是为了用户体验:它会在遇到 DELETE FROM … 时用一句话说明原因并拒绝执行,而不是返回一个需要模型去解码的数据库错误。它不是边界——基于文本的黑名单总是只差一个大小写就会出错,最典型的例子是 SELECT * INTO audit_copy FROM users:它以 SELECT 开头,不包含任何被禁止的动词,却创建了一张表。

真正的边界是数据存储:SQLite 上使用 mode=ro 加 PRAGMA query_only,PostgreSQL 上使用 READ ONLY 事务,并且两者都设置了语句超时。测试完全绕过守卫,确认连接仍然拒绝执行。

列脱敏是唯一一个真正算作强制措施的文字层控制:deny_columns 中的值会在获取之后、结果字符串存在之前被丢弃,因此 SELECT * 也无法泄露它们。所有返回的内容都用 <untrusted> 标签包裹,因为一个看起来像指令的 notes 列本身就是数据,必须被标记为数据后再到达。

CLI

groundtruth [--config PATH] <command>

  doctor                     what is wired up, what is missing
  targets                    the configs this project exposes
  lint TARGET                exit 1 on errors
  replay TARGET --seed N     one deterministic run, full trace
  simulate TARGET            --runs N --seed N --gate --check-determinism
  query "SELECT ..."         one read-only statement
  schema                     readable tables and columns
  serve                      the MCP server, over stdio

退出码:0 表示干净,1 表示有发现(lint 错误、阈值超出其区间、非确定性),2 表示无法运行(配置错误、缺少能力、查询被拒绝)。可以为 lint、replay 和 simulate 添加 --json 以输出机器可读的结果。

安装

pip install groundtruth-mcp          # core: rules, simulation, gating, CLI
pip install "groundtruth-mcp[mcp]"   # + the MCP server
pip install "groundtruth-mcp[postgres]"  # + the PostgreSQL data source

Python 3.11+。核心没有任何第三方依赖——这是有意为之,这样 CI 门禁就不依赖智能体技术栈。一个独立的运行器无需安装 SDK 就能强制执行你的阈值。

验证接线

python scripts/mcp_smoke.py [path/to/groundtruth.toml]

它会将服务器作为真正的子进程启动,通过 stdio 初始化,列出工具,调用其中两个,并打印返回结果——与客户端执行的序列完全相同。在指责智能体看不到你的工具之前,先运行它。

CI 对每个拉取请求强制检查什么

这可不是一个“测试已经运行”的徽章——六项检查,每一项都曾经拦截过问题:

检查项

为什么它是门禁而不是建议

ruff check + ruff format --check

包含 BLE,因此每个宽泛的 except 都必须有书面理由

mypy

包附带 py.typed;错误的注解就是错误的 API

pytest 在 3.11 / 3.12 / 3.13

69 个测试,覆盖率下限 75%(当前为 78%,含分支覆盖率)

scripts/mcp_smoke.py

真实的子进程、真实的 stdio、真实的 tools/list 和 tools/call

simulate --gate --check-determinism

项目自己的参数,应用于自身

lint broken_checkout 必须退出 1

不会失败的 lint 只是装饰品

限制,直说

  • SQL 表允许列表是基于文本的。 它会扫描 FROM 和 JOIN 之后的标识符。真正的按表强制是数据库授权;这只是一个带有良好错误信息的护栏,真正起作用的是只读事务。

  • 关键词黑名单会在字符串字面量内部进行匹配。 对包含 grant 的值进行过滤的查询会被拒绝。修复这个问题需要一个真正的 SQL 解析器,而当解析器不是边界时,不值得构建它。

  • 自动 LIMIT 是一种启发式方法。 子查询中的 LIMIT 会抑制顶层追加。max_rows 仍然限制渲染的内容。

  • 选择器不进行过滤。 states[].transitions[] 会遍历所有内容;没有 states[kind=terminal] 这样的写法。谓词语言会是第三个没人要求的功能。请改用 @kit.validator。

  • 阈值是项目级的,而不是按目标设置的。 项目中的每个目标都按相同的区间来评判。配置确实需要不同区间的项目,应该使用独立的 groundtruth.toml 文件。

  • PostgreSQL 数据源已实现,但测试较少——测试套件针对 SQLite 验证了边界,因为 SQLite 无需服务容器就能在任何地方运行。

这个项目的由来

该模式提取自一个私有代码库,并在那里确立了自己的地位:一个创作管道,其贡献者不断交付通过模式验证却会在运行时崩溃的配置。领域特定的部分留在了原地。被泛化的是其形态——检查、重放、模拟、查询——以及一系列被证明比功能列表更重要的决策:

  • 单一阈值列表,由代理和 CI 共同读取,因为两份副本会漂移,而工具曾一度对 CI 会拒绝的数字报告 PASS。

  • 错误信息中内联列出合法的替代项,因为代理如果必须发起第二次调用才能知道它能传递什么,就会去猜测。

  • 工具描述由实时配置生成,因为过时的描述会让代理错误而自信地使用该工具。

  • 每条路径都限制输出上限,因为一次过度的查询就可能挤掉对话的其余部分。

docs/ARCHITECTURE.md 包含模块地图和完整论证。

许可证

MIT。

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to search code by meaning, explore codebase structure, store and query knowledge with temporal facts, and read source code through a set of MCP tools.
    310 npm
    7
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables coding agents to perform file, search, patch, git, process, test, package, network, and system operations through 60 typed MCP tools with structured inputs/outputs, structured errors, and a full event journal, replacing terminal use with a typed machine API.
    MIT