首页 / 文章

模式无法为智能体提供的信息:BigQuery上的开放知识格式

一项基于BigQuery和Gemini ADK的实验:将开放知识格式的文件存储在Git中,使智能体获得了架构无法提供的信息,从而稳定了答案质量,并修正了29%的GMV误差。

发布者:REACTAPP.TOP阅读约 13 分钟2961 词

来源: **What Your Schema Cannot Tell Your Agent, Open Knowledge Format Can**

无人负责的提示文件

将人工智能代理与数据仓库连接后,迟早会出现一些常见的配置内容。起初只是系统提示中的三四条简明语句:将退款排除在收入之外、财年从4月开始、忽略内部测试账户。几个月后,这份文件的内容量便增长到约四百行。虽然有工程师敢于对其进行修改,但真正负责指标定义的分析师却从未见过它。

文件体积庞大反而是次要问题,真正棘手的是没人能够维护它。这类提示字符串没有指定的负责人,也没有审核流程或有效期。即便财务部门在1月出台了新的收入政策,工程师也不会得知,分析师也无法修改代码,而这些提示文件更不会自动失效。

在那种摩擦作用下,知识会变得过时或永远无法完成,因此智能体只能依靠不完整的定义和隐含的假设来运行。而这些缺失并不会导致智能体明显出错——没有崩溃,没有SQL语法错误,也没有空结果返回。智能体会检查原始架构,推测缺失的业务规则,然后生成一个整洁且可信的数据结果,最终呈现在管理层的仪表板上。原本只是维护上的麻烦,却悄悄演变成了准确性问题。

开放知识格式(OKF)正是为填补这一缺陷而设计的。业务规则不再存在于提示字符串或不透明的向量存储中,而是以带有YAML前置信息的普通Markdown文件形式存储在Git中。所有权变得清晰明确,任何更改都需要通过拉取请求来完成,而且到期日期对分析人员和工程师来说都是可见的。

怀疑论者认为,更强大的模型和更大的上下文窗口使得这种做法毫无意义,因为可以直接将原始架构和文档交给智能体,让其自行推断定义。一项简短的实验对不完整知识所带来的成本以及OKF基础设定变化进行了量化评估。该实验使用了两个BigQuery表格、一个业务问题,以及基于Google的Agent Development Kit(ADK)在多种知识设定下构建的智能体。

测试场景

测试环境为一个在线市场,其中包含orders表格和payments表格,同时还有一个产品经理在每周一早上可能会提出的问题。

2026年7月,Jabodetabek地区通过QRIS支付成功完成的交易为该地区带来了多少GMV?

有三个术语需要解释。GMV指的是商品总价值,即某一时期内销售商品的总金额。QRIS是印度尼西亚的全国性二维码支付标准。Jabodetabek指的是雅加达大都会区,包括雅加达、博戈尔、德波克、丹格朗和贝卡西。

问题所需的每一项数据都在数据库中存在,但现有架构却无法回答该问题。这个问题中隐藏着四个决策,每个决策都是由人工做出的:

  • 证明款项已到账的状态。支付记录同时包含status_code和transaction_status。看似有用的status_code = 200仅表示网关调用成功,其中也包括取消的情况。只有当transaction_status = 'settlement'时,资金才真正到达商家的账户。这些预设值是仿照真实支付网关的公共API设计的。
  • 有效的订单。已完成的退货订单对应的已结算付款实际上属于资金回流,但在退款处理完成之前系统仍会将其标记为settlement状态。因此筛选条件需要设置为orders.order_status = 'completed',而现有架构并未提供相关提示。
  • 确定时间范围的时间戳。订单的结算处理可能会滞后数小时甚至数天。通过orders.order_ts筛选7月的订单会得到一个总数,而通过payments.settlement_ts筛选则会得到另一个数值,因此7月31日的订单可能会被归入8月。
  • 构成区域的城市。系统中没有region字段。雅加达大都会区必须用13个精确的destination_city值来表示,而Kota与Kab.则代表两个不同的地点,而非同一名称的不同拼写形式。

即便其中任何一处出现错误,依然会生成并执行查询。没有错误提示,没有空结果,也没有堆栈跟踪信息,只有格式正确的数字会被传递给处理系统。

上下文与知识

显而易见的解决办法是把那四个决策写下来,并连同架构一起提供给模型。一个功能强大的模型通常能够据此推理出正确答案。但它在下一次会话中仍需重复这一过程,而且并没有什么机制能确保两次会话的结论一致。GMV的定义并未存储在任何地方,而是由一个不会记住前一天结论的流程按需重新推导出来的。

这一差异决定了后续的所有讨论方向。上下文是模型进行推理的素材,而知识则是他人已经得出并认可的结论。每次会话都需要重新进行推理,而书面结论只需支付一次成本即可。

那要写在哪儿呢?由于上述原因,不会写在提示字符串中。数据库或向量存储虽然能提供存储空间,但会带来审查问题,因为分析人员不会针对嵌入存储提交拉取请求。

OKF是什么

谷歌提出的方案就是OKF,它的设计极为简单。一个包只不过是一个包含YAML前置信息的Markdown文件文件夹,通过Git进行共享,无需任何数据库、向量存储或SDK。每个文件描述一个概念,type是唯一必填的前置信息键,而普通的Markdown链接则将这些文件连接起来,从而使这个包形成图结构而非单纯的文件堆叠。

更重要的问题是其中包含的内容是什么。类型并未在中心位置进行注册,使用者必须接受那些他们不认识的类型,因此以下内容只是初始词汇表而非完整架构。在谷歌发布的两个示例数据包中,目录结构如下:

  • tables/ 和 datasets/ 用于说明数据的含义:每一行代表什么,列之间如何关联,存在哪些枚举值。如下一步所示,这部分信息可以从数据仓库中获取。
  • metrics/ 用于解释数值的含义。GMV及其相关条件都存储在这里。
  • references/ 存储那些没有任何对应列的领域知识,例如雅博德塔贝克地区的13个城市、产品分类体系或内部测试账户列表。
  • policies/用于存储定义所引用的权威文档,例如财务部门每年1月重新发布的收入确认规则。
  • computations/和attesters/保存某个指标的经过审批的SQL代码,以及用于验证结果确实来自该SQL的代码。
  • skills/记录相关流程:何时使用某项内容、其前置条件以及操作步骤。
  • 判断某项内容是否应放入某个文件包的简单标准是:是否会在新分析师入职的第一周向他们讲解?如果是,则将其放入相应文件中。

    该规范的 v0.2 版本新增了三个前置字段,可直接解决上述问题。sources 字段列出了某个概念的来源。verified 字段用于记录形如 {by, at} 的确认事件,没有此类记录的概念会被标记为 unverified。stale_after 字段则指定了该概念被视为过时的具体时间点。这些字段共同提供了负责人、审核机制以及有效期信息,所有内容都保存在同一个文件中,分析人员可对其进行编辑,审核人员则可通过拉取请求查看这些内容。

    团队能否自行设计文件夹结构?当然可以,但统一的术语规范能实现私有结构无法实现的功能。下面介绍的参考代理能够直接生成半套文档,因为它已经理解了BigQuery Table的概念。审核人员也清楚签署verified状态意味着要承担什么责任。此外,该格式是供应商中立的,不依赖任何代理、框架或模型提供商,因此今天编写的知识在技术栈变更后依然有效。

    许多读者会认为这是一种包含额外步骤的智能体技能,而且两者之间的重叠确实足够值得探讨。OKF将type: Skill视为一种概念类型。Google的示例包中,skills/run-on-bq.md文件从“使用场景”、“前提条件”和“步骤”三个方面进行了说明,这与普通的智能体技能十分相似。技能是OKF的子集而非同义词——技能用于指导智能体如何行动,而OKF则负责解释各项术语的含义、最后谁检查过这些信息以及答案的有效期限。使用该框架需分为三个步骤:生成仓库已掌握的信息、编写仅人类知晓的内容,最后让智能体同时读取这两类信息。

    第一步:生成仓库已有的信息

    当BigQuery表已有文档记录时,那些重复性的工作就无需手动完成了。OKF仓库中包含一个用于创建文档的智能体,只需一条命令即可将BigQuery数据集转换为对应的文档包。

    uv tool install git+https://github.com/GoogleCloudPlatform/open-knowledge-format
    
    reference-agent enrich \
      --source bq \
      --dataset "$PROJECT_ID.marketplace" \
      --out bundles/generated \
      --no-web
    

    它会生成一个datasets/文档、每个表格对应的tables/文档,以及定期更新的index.md文件。添加--no-web参数可以跳过合并现有文档的可选步骤,从而使首次运行结果可重复。

    实际效果优于预期。它能描述每个表格的详细结构,找到正确的连接键,并记录状态枚举值。由于BigQuery数据源仅提供数据集和表格的概念,因此很少会生成Metric或Reference文档。其边界范围取决于数据仓库本身已记录的自身信息。

    在认定其他仓库表现相同之前,请先验证这一点。状态值会出现在生成的文档中,只是因为有人将它们写入了DDL的列描述中。如果让生成器处理那些没有文档记录的纯列,它可参考的信息就会少得多。

    生成的文档可能包含错误,实验中就有这样的例子:有一次运行产生了包含sum(t2.total_price)的查询语句,但实际上并不存在这样的列。该列的真实名称是gross_amount,同一文件在前面两节的自定义模式表中已正确标注了这一名称。使用该查询语句会失败。

    这就是verified功能的作用所在。每份生成的文档都会记录generated: {by: reference_agent/...},但不会记录verified事件,因此该格式本身就会将其标记为未经审核。文档包会记录自身的来源信息,而将机器输出与人工决策区分开来也无需任何特殊处理。

    第二步:记录只有相关人员才知道的信息

    在同一文档包中采用相同格式存放两份手写文件,就足以记录下那四个隐藏的决策信息。metrics/gmv.md是其中更为重要的文件,其前置信息展示了信任字段的具体位置。

    ---
    type: Metric
    title: Gross Merchandise Value (GMV)
    description: Total settled transaction value for a period in IDR, excluding cancellations, reversals and returns.
    tags: [metrics, finance, headline-metric]
    generated: { by: human:analytics-lead@marketplace.example, at: 2026-09-01T09:00:00+07:00 }
    verified:
      - { by: human:analytics-lead@marketplace.example, at: 2026-09-01T11:00:00+07:00 }
    stale_after: 2027-01-31T00:00:00+07:00
    ---
    

    文档中明确列出了全部四个条件,并指出该架构并未提供任何条件。结算状态意味着transaction_status = 'settlement',而非status_code = 200。订单的状态必须为order_status = 'completed',且已取消的订单除外。在此时间范围内,settlement_ts是截止时间,通过带有+07时区偏移的半开区间进行判定:从包含2026-07-01 00:00:00+07到不包含2026-08-01 00:00:00+07。若直接对日期字符串使用BETWEEN操作,则会忽略最后一天,并将所有时间戳视为UTC时间,因此每个时间边界都会向后推移七小时。

    stale_after 这一参数真正发挥了作用。它仅用一行代码就解决了1月的问题:财务部门每年1月都会重新发布收入确认规则,因此相关定义会在2027年1月31日失效,系统会要求使用该失效后的定义进行报告,而非显示当前的有效数值。

    第二个文件 references/jabodetabek.md 列出了13个确切的 destination_city 字符串,并警告不要使用 LIKE '%Jakarta%' 这种查询方式,因为这样会悄悄遗漏其中的8个城市。

    在这里,协作变得具体可感。重新定义GMV现在只需针对Markdown文件提交拉取请求,负责的分析人员可以对其进行审核,无需立即编辑,也不需要重新部署后端系统。谷歌的示例包甚至保留了已过时的 gross-margin-legacy.md 文件,这样历史数据依然可以复现,而简单的字符串查询则无法做到这一点。

    第三阶段:读取这些数据的代理

    在数据访问方面,该代理使用位于 https://bigquery.googleapis.com/mcp 的 Google Cloud 托管型 BigQuery MCP 服务器,一旦为项目启用了 BigQuery API,该服务器便会自动启动,无需额外部署任何内容。

    项目中唯一的自定义代码是一个用于读取文件的工具。

    def read_okf_concept(path: str) -> dict:
        """Read one OKF concept document from the local bundle.
    
        Args:
            path: Path relative to the bundle root, for example "metrics/gmv.md".
                Call this with "index.md" first to find which concepts exist.
    
        Returns:
            'status', and on success 'content' with the raw markdown and frontmatter.
        """
        doc = (OKF_BUNDLE / path.lstrip("/")).resolve()
        # `path` is model-controlled. Block anything that escapes the bundle.
        if not doc.is_relative_to(OKF_BUNDLE) or not doc.is_file():
            return {"status": "error", "error": f"no concept at {path}"}
        return {"status": "success", "content": doc.read_text(encoding="utf-8")}
    

    然后定义代理本身。

    bigquery_mcp = McpToolset(
        connection_params=StreamableHTTPConnectionParams(
            url="https://bigquery.googleapis.com/mcp",
            headers={
                "Content-Type": "application/json",
                "Accept": "application/json, text/event-stream",
            },
            sse_read_timeout=300.0,
        ),
        header_provider=_bigquery_auth_header,
        tool_filter=[
            "list_dataset_ids",
            "list_table_ids",
            "get_dataset_info",
            "get_table_info",
            "execute_sql_readonly",
        ],
    )
    
    root_agent = Agent(
        name="marketplace_analytics_agent",
        model="gemini-3.7-flash",
        instruction=(
            "Before writing any SQL: call read_okf_concept('index.md'), then read every "
            "concept the question touches: the metric, the payment status, the geography.\n"
            "Use only the filters, joins and period cuts those concepts define. "
            "Never invent a status value or a metric formula.\n"
            f"Run queries with execute_sql_readonly, passing projectId='{PROJECT_ID}'.\n"
            "Show the SQL you ran, and name the concepts you relied on."
        ),
        tools=[read_okf_concept, bigquery_mcp],
    )
    

    实验随后进行了这段对话测试。

    实际发生了什么

    在各种配置下,智能体、问题以及初始数据均保持不变。只有OKF_BUNDLE所指向的目录会改变,且每种配置都会运行四次。

    在没有知识层的情况下,读取工具指向的是一个空文件夹,只能依靠MCP服务器的架构自检功能,对于同一问题的四次测试得出了三个不同的标题数值:371,251,000、333,020,000和371,886,000。其中最高的数值比真实值高了29%。

    使用生成的捆绑包并执行一次reference-agent enrich操作后,数值不再出现波动。每次得到的结果都是329,590,000,数值稳定且可重复,但仍比真实值高15%。

    加入两个手工编写的概念后,每次得到的数值为286,740,000。这个数值是正确的,而且答案还会注明其所依赖的概念。

    其发展路径很明确:回答的准确性取决于所提供的知识有多精确。在没有相关层次结构的情况下,智能体每轮都需要自行猜测缺失的逻辑,因此错误率会高达29%,且波动较大。通过机器可推导的架构上下文可以消除这种不稳定性,将错误率降至15%,但它无法单独完成全部工作,因为剩下的问题属于业务政策层面而非技术层面。只有对那些不成文的规则——如确切的时间戳范围和订单状态——给出明确且经过人工审核的定义,智能体才能可靠地达到精确的准确率。

    事实证明,提升准确率的关键既不是更长的提示语,也不是更大的模型,而是将结构化、经过版本控制的领域知识放置在数据库架构的终点处。

    这种方法无法解决的难题

    这种约束依赖于指令,因此模型可以自由选择忽略它,而且也没有任何规定禁止在读取某个概念之前就编写SQL代码。那些需要严格保障的人可以添加ADK的before_tool_callback函数,如果在某次轮次中未读取任何概念,则拒绝该查询请求。

    Markdown同样不进行模式验证。分析人员可能会提交格式错误的概念,而OKF故意保持宽松态度,要求使用者容忍未知类型和失效链接。真正的保护措施来自拉取请求时的代码审查,以及verified和stale_after机制,这些机制会在代理读取定义时暴露出未经审查或已过期的定义。该规范将信任等级视为建议性信号,因此是否使用这些信号由构建代理的人决定。这里的代理会读取这些信号,其他系统也需要如此处理。

    这些数据也不是基准值:每个设置仅使用一个问题、一个模型,并在旨在设置陷阱的种子数据上运行四次。它们仅展示了某种机制,却无法说明OKF对准确率的平均影响。

    依然有效的经验

    这种机制仍有价值。当数据处理工具出错时,结果看起来并不异常——没有异常触发,没有空结果返回,也没有任何红色提示。SQL语句有效,图表看起来也合理,因此会在有人产生怀疑之前就被记录在演示文稿中。

    最需要警惕的错误并非那种不稳定的错误。在没有指导的情况下,该工具每次运行都会返回不同的数值,这种问题很容易在短时间内被发现。而当它只拥有部分知识时,虽然每次运行都会返回相同的数值,却还会随意提及一个无法证明其合理性的替代方案。稳定的结果看似能证明正确性,但实际上并不能证明什么。

    一个更完善的模型并不一定能填补这一差距。有一个指令就能涵盖仓库所能提供的所有信息,且恰好在人类决策开始的地方停止:界定时间范围的时间戳、用于统计的订单状态、表示款项已到账的状态。这些都是人为选择而非客观事实,应当由做出决策的人在某个地方记录下来,以便日后可以向其询问相关情况。

    一个好的开始方式是选择规模较小的项目。挑选团队经常争论的指标——那种两个仪表板显示结果不一致、但大家都清楚该咨询哪位分析师的指标。用相关数据集运行生成器,然后将定义及其负责人信息写在同一份文件中,并通过一个拉取请求提交。真正的核心工作在于这些讨论,而格式只是为它们提供一个归宿,记录下谁在何时达成一致以及有效期。工具部分是最乏味的。有趣的是当分析师和工程师必须就一句话达成一致并予以确认时会发生什么,以及哪些隐含的决策最终由他们来决定。

    相关阅读

  • 当RAG以0.89的相似度检索到错误文档时:邻近性与相关性 — 为何高余弦值仍可能指向错误内容,欧几里得几何如何影响层级与时间关系,以及哪些缓解方法有效:混合搜索、重新排序、图结构。