Phoenix Analytics SQL:面向 Agent 的只读 SQL 分析接口设计与实现 可观测性AI 评测LLMOpsAI 应用人工智能【免费下载链接】phoenixAI Observability Evaluation项目地址https://gitcode.com/gh_mirrors/phoenix13/phoenix点击查看免费下载导读Phoenix 通过 GraphQL 与 REST API 对外暴露数据但这些 API 只能回答固定形态的问题。当 Agent 提出本周哪个模型 p95 延迟最差、哪些提示词触发了最多重试这类分析性问题时固定的 API 无从回答。本指南深入解析 Phoenix 仓库中 Analytics SQL 这一 MCP 分析面design 文档见 internal_docs/specs/mcp-analytics-sql.md实现位于 src/phoenix/server/mcp/sql/说明它如何在不让 Agent 越权读取表、不让语句永不终止、不让返回数据撑爆上下文窗口的前提下允许 Agent 用 SQL 表达任意分析问题并直接获得答案。读完本文你将掌握该面从解析、准入、重写到执行背书的完整六阶段管线能力边界行数、字节、截止时间、并发的设计意图两个 MCP 工具describeSqlSchema与executeSql的用法与返回结构以及它如何在 SQLite 与 PostgreSQL 两个后端之间裁决分歧是否算缺陷。一、问题固定 API 之外的即席分析需求Phoenix 以遥测telemetry、数据集datasets与实验experiments为核心数据。既有 API 均回答固定的问题集。一个 Agent 若被问到某个没有人预先设想过的分析性问题——例如这个项目本周哪个模型 p95 延迟最差、哪些提示词产生的重试最多——它没有现成的查询入口。它只能自己翻页拉取 spans 并自行聚合这需要大量往返与上下文开销或者干脆放弃。数据库本身可以直接回答这些问题。所缺的只是让 Agent 安全地发问的途径既不能让它读到不该读的表也不能让它运行永不终止的语句更不能让它返回超出上下文窗口容量的数据。二、目标与非目标目标让 Agent 以 SQL 表达分析性问题并直接获得答案对任何单条语句的读取量、成本与返回量设界把 schema 描述得足够好使模型第一次就能写出正确的 SQL让 schema 描述足够廉价schema 发现不会主导调用方的 token 预算。非目标spec 原文不是机密性边界见威胁模型章节不支持写操作、DDL 或事务控制不支持跨数据库或联邦查询不做引擎之外的查询优化不缓存结果不提供存储或命名查询。三、正确性判据谁编写了行为该面上几乎所有正确性争议都以同一种形式出现调用方得到一个意外结果或两个后端对同一语句给出不同答案需要判定这是否为缺陷。一个提问即可定论产生该结果的行为是谁编写的。该面自己编写的行为归该面负责修复引擎编写的行为归引擎负责调用方有权依赖其文档。该面编写的行为引擎编写的行为语句的含义我们改变了含义就由我们修复引擎不同差异就透传可用的能力概念是我们的两个后端必须一致能力不同允许能跑的一侧在另一侧给出可执行的拼写并拒绝具体化为四条规则答案必须回答被提出的问题。语句可以被自由重写——展开星号、替换派生列、注入 limit——但绝不能使结果偏离调用方 SQL 的本意。案例SQLGlot 把调用方的json_extract渲染成-返回 JSON 文本而非值导致MAX按字典序比较。这是该面管线改变了含义由该面修复。该面发明的概念只能有一个含义。latency_ms与graphql_node_id在别处不存在没有任何规范定义它们包外也无法校验。案例latency_ms在两个后端计算出不同数值这是该面的责任——两个实现必须彼此一致。引擎行为透传。案例-在 SQLite 返回类型化值、在 PostgreSQL 返回文本因此对数值 JSON 路径求max一端得到130000另一端得到9。两个引擎都按规范行事该面既不做调和也不拒绝但结果信封会在未加 CAST 的排序敏感提取处给出警告见 rewrite.py 中_note_uncast_json_ordering其按方言区分的_JSON_TEXT_ORDERING_NOTES注释给出了两端的具体示例。能力可以不同拒绝必须可执行。案例percentile_cont(...) WITHIN GROUP在 SQLite 没有对应语法。这是引擎的能力缺口因此该构造在 PostgreSQL 上被准入、在 SQLite 上被拒绝且拒绝消息点名给出percentile(x, p)作为替代拼写。四、威胁模型与能力边界这是塑造其余设计的最关键决定且最初在代码中被表述错误。该面对任何能触达 MCP 挂载点的调用方开放可读取遥测、数据集与实验。Phoenix 本已允许任何已认证用户通过 GraphQL 读取所有这些仅四个查询携带IsAdmin——users、user_api_keys、oauth2_grants、system_api_keys——而这些表均不在本面白名单内projects及其可达的所有表则根本不带权限类。因此此前的 ADMIN/SYSTEM 检查比旁边的 API 更严格在 SQL 中拒绝的同一调用方却可在 GraphQL 中取到。该检查已被移除源码见 tools.py 中register_analytics_sql_tools的文档字符串。约束该面的是能力capability而非身份identity约束值语句形态单条只读语句SELECT、UNION、INTERSECT、EXCEPT行数默认 500最大 5000字节每行 256 KiB每响应 4 MiB截止时间30 秒PostgreSQL 用statement_timeoutSQLite 用 progress handler并发PostgreSQL 4 并发、SQLite 1 并发队列深度 8以上常量在 execute.py 中有精确对应DEFAULT_ROW_LIMIT 500、MAX_ROW_LIMIT 5_000、BYTE_LIMIT 262_144、MAX_RESPONSE_BYTES 4 * 1024 * 1024、MAX_SQL_BYTES 2 * 1024单次调用 SQL 文本上限 2 KiB超长在未执行前即被拒绝、PG_STATEMENT_TIMEOUT_MS 30_000、SQLITE_TIMEOUT_SECONDS 30并发表为SQL_CONCURRENCY {postgresql: 4, sqlite: 1}队列深度由StatementAdmissionController._queue_size 8承载。该设计要防御的对手是被误导或被劫持的模型而非试图获取本无法获取之数据的恶意用户。上述每项控制都在限制爆炸半径没有一项在限制调用方通过其他途径同样能拿到的信息。两个随之而来的推论都是有意为之表就是数据边界。白名单表内的每个物理列都可查询被排除在面外的数据通过不将其表列入白名单而排除。争用是残余风险。SQLite 执行宽度为 1一条慢查询会将其余所有分析请求串行化直至其截止时间。许可证permit在解析之前获取因此准入与重写也要排队等待。行数限制、字节上限与截止时间约束的是单条查询不约束争用。设计中未处理的一个独立问题spans.attributes与spans.events包含被追踪应用写入的文本通常即该应用的终端用户写入的文本结果被返回给同一个 MCP 服务器上持有破坏性工具的模型而该面没有把返回的行标记为不可信内容。这是已记录在开放问题中的缺口。五、架构六阶段管线一条语句依次经过六个阶段一个解析、两个策略闸门、一个变换、一个文本生成最后一个交给引擎自带的背封backstop。caller SQL │ ├─ 1. parse SQLGlot单条语句SELECT 或集合操作 ├─ 2. admission 对解析树做白名单校验 ├─ 3. rewrite 星号展开、派生列、时间戳算术与字面量、 │ JSON 规范化、schema 限定、limit 注入 ├─ 4. post-rewrite 关系与 schema 限定复检 ├─ 5. render 把树重新生成为 SQL 文本 │ └─ 6. execution SQLiteauthorizer 回调 PostgreSQLEXPLAIN 计划闸门调用方的文本只在阶段 1 被读取一次此后永不再读。之后的每个阶段都工作在树上。数据库实际运行的语句在阶段 5 由该树生成意味着数据库永远不会看到调用方输入的原样文本。阶段 5 是无条件的不存在任何路径让未修改的语句以文本形式透传因为 limit 注入与 schema 限定作用于每条语句。一条语句的端到端旅程这是真实 trace 而非示意图。调用方提交SELECT latency_ms FROM spans WHERE name chatlatency_ms是虚拟列被替换为按方言的表达式整个替换以括号包裹因此绑定位置与普通列完全一致SELECT (EXTRACT(EPOCH FROM (end_time - start_time)) * 1000) AS latency_ms FROM spans WHERE name chat随后 schema 限定把spans解析到连接对应的 schemalimit 注入追加row_limit 1使截断可被检测而非被假设。以下是 PostgreSQL 实际收到的字符串SELECT (EXTRACT(EPOCH FROM (end_time - start_time)) * 1000) AS latency_ms FROM public.spans WHERE name chat LIMIT 501这条语句在五个阶段中几乎无事可做没有*可展开没有 node id没有时间戳字面量比较没有时间戳相减JSON 规范化仅限 SQLite。信封envelope如实报告实际触发的三个重写。最终执行的字符串与调用方提交的不同——它从树打印而来而非从调用方文本编辑而来。该设计依赖的每条性质——limit 存在、只出现白名单关系——都是树的性质且仅因为执行语句由该树打印而对执行语句成立。六、阶段 2准入Admission准入作用于解析树而非文本。四个维度是白名单——节点类、按方言的函数类与函数名、关系、CAST 目标类型。基表列是第五个正向目录对给定方言每个引用必须命名打包 DDL 资产中的物理列或适用的虚拟覆盖层。未知名称在未执行前即被拒绝并就近给出物理与虚拟名称作为更正建议parse.py 中用difflib.get_close_matches实现。查询局部关系则不同。CTE、子查询、输出别名或表值函数自行定义列这些名称无法对照基表资产校验。既包含此类源、又存在未限定引用的作用域中若该源可能投影出该名称则允许限定基表引用仍然失败关闭fail closed。标识符匹配遵循目标引擎SQLite 标识符大小写不敏感PostgreSQL 将未加引号的标识符折叠为小写并保留带引号的拼写因此 DDL 加载器保留了哪些物理列曾加引号。NATURAL JOIN被拒绝因为其隐式键会随物理 schema 演进而变化整行与复合字段引用也被拒绝以让策略继续在显式列上推理。七、阶段 3重写Rewrite——九个按序执行的 pass九个 pass 的顺序是承重的而非偶然rewrite.py 的rewrite()入口依次调度它们星号展开——*变成有序的物理 DDL 列后接适用的虚拟覆盖层。放在第一位是因为它产出latency_ms与graphql_node_id供下两个 pass 解析若颠倒顺序会把这些列原样送进引擎。latency_ms——替换为按方言的表达式。SQLite 上基于time_subtime 扩展的纳秒级减法PostgreSQL 上为EXTRACT(EPOCH FROM (end_time - start_time)) * 1000。graphql_node_id——在谓词中解码、在投影中构造类型通过限定符逐引用解析。相等、IN/NOT IN、 ANY/ ALL、IS [NOT] DISTINCT FROM变成主键上的整数比较LIKE、BETWEEN不是 node id 语义保留投影形式。时间戳相减仅 SQLite——end_time - start_time通过unixepoch重写避免引擎做文本算术。时间戳字面量——与时间戳列比较的字面量按后端正确比较的布局重新输出。PostgreSQL 自解析字面量因此实际仅 SQLite 生效两端对裸日期都会在notes中记录按 UTC 读取。此外无偏移的时间字面量在准入即被拒前置规则要求写2026-07-01T14:30:00Z这样的形式。JSON 规范化仅 SQLite——访问器改写为部署中表达式索引所用的拼写json_extract 全引号路径。SQLite 按解析后的表达式匹配索引改写使调用方写$.a.b也能命中 SQLAlchemy 编译出的json_extract全引号拼写-返回 JSON 文本、-与json_extract返回值因此绝不把-改写成函数形式以免改变比较语义。PostgreSQL 动态 JSON 提取——WORKAROUND sqlglot30.15.0。SQLGlot 在键非常量路径时把jsonb - expr渲染为json_extract_path而 PostgreSQL 的json_extract_path接受json而非jsonb。这些节点被改写为jsonb_extract_path/jsonb_extract_path_text字面量键attributes - llm仍渲染为-。当 sqlglot 固定版本越过 30.16.0 时可移除该 pass不输出外部链接仓库中检索WORKAROUND sqlglot30.15.0即可定位所有相关站点。Schema 限定——白名单关系限定为解析出的 PostgreSQL schema。Limit 注入——追加row_limit 1使截断可检测而非被假设。此外rewrite 末尾会运行 JSON 文本排序检查_note_uncast_json_ordering当MIN、MAX、ORDER BY及排序类比较作用于未 CAST 的 JSON 提取时按方言给出警告注释——PostgreSQL 上#返回 text 导致1017066排在149740之后SQLite 上则取决于文档中值的类型。SUM/AVG会强制类型转换故被有意排除在警告之外。八、阶段 4 与阶段 6复检与引擎背封阶段 4post-rewrite 检查验证重写后的树只引用白名单关系且若带 schema 限定符该 schema 正是表所在 schema。它严格弱于准入且无法做到与准入相等重写有意发出准入会拒绝的 SQL——如匿名函数encode、convert_to。实际保证的性质是没有出现新关系而非仍然可准入。这是一个已知弱点已记录在开放问题中。源码中对应_assert_rewrites_preserved_policy它以拒绝而非断言的方式工作使失败通过错误信封离开而非作为未处理异常逃逸。阶段 6引擎背封SQLite 的 authorizer 回调与 PostgreSQL 的EXPLAIN计划闸门看到的是渲染之后的语句这与准入看到的不是同一件事json_extract(x, path)已作为-运算符发出调用方从未写过的函数以 SQL 名出现。二者并不等价。SQLite authorizerexecute.py 中_sqlite_authorizer在任何位置拒绝任何非白名单函数且拒绝记录具体被拒的标识符——因为 SQLite 对驱动报告的只是一条无法区分的通用 operational errorauthorizer 是唯一知道哪个标识符被拒的地方。它还拒绝一切改变状态的 actionSQLITE_INSERT、SQLITE_UPDATE、SQLITE_DELETE、SQLITE_ATTACH、DDL 全家等拒绝读取数据库目录表sqlite_master等并利用第五个参数via区分直接基表读取与经由视图/触发器的读取——视图即使与白名单表同名也一律拒绝因为回调看不到视图的定义。计划闸门verify_postgres_plan则检查关系与集合返回节点并从ProjectSet的表达式文本中读取标量函数名——这无法区分函数与关键字一个EXTRACT(epoch FROM ...)曾因内部括号被误报为名为from的函数见_NOT_A_CALL列表的注释也根本看不到普通标量调用。因此准入函数策略上的漏洞在 SQLite 有第二层兜底在 PostgreSQL 则没有。九、设计决策详解决策能力按后端划分答案分歧不算缺陷函数策略是带声明差异的并集而非交集。每个后端获得其能做的——SQLite 的percentile、julianday、json_eachPostgreSQL 的 ordered-set 聚合与 JSONB 面——叠加在 35 个可移植节点类之上。把面裁剪到两引擎之较小者会为对称性而删除两端的真实能力。JSON 面按数据所在位置而非可移植性来定尺寸该部署存储的几乎所有东西都在spans.attributes读取文档就是对其大部分提问方式两端都获得各自 JSON 词汇表中纯且受文档边界约束的部分。PostgreSQL 是键存在?、?|、?、包含、、路径测试?、jsonb_path_exists、jsonb_path_match、路径查询、键枚举以及尺寸/渲染/构造函数SQLite 是 json1 对应物json_array_length、json_valid、json_pretty、json、json_quote、json_array、json_object与两个分组聚合。聚合进单文档的能力两端都准入PostgreSQL 的jsonb_agg/jsonb_object_agg与 SQLite 的json_group_array/json_group_object四个都会放大N 行塌缩进一个随 N 增长的单格按group_concat已确立的条款准入——每格字节上限拒绝超大结果截止时间约束工作量。产生修改副本也被准入jsonb_set、jsonb_insert、#-与 SQLite 的json_set、json_insert、json_replace、json_remove、json_patch因为它们只返回受输入约束的新文档且在大成员跨过每格字节上限前移除它正是该面想要的用途。剩余的才是引擎表达能力的真实差异。SQLite 没有键存在/包含运算符、没有 SQL/JSON 路径函数因此这些问题在 SQLite 上用json_extract(...) IS NOT NULL与json_each来问。该拒绝被记录在语料中——按规则未声明的非对称与缺口不可区分。手维护集合中的缺口在有人写出它遗漏的语句前是不可见的且随之而来的拒绝点名的是解析器类而非调用方写的东西?报jsonb_contains并非 PostgreSQL 对它的函数名?报j_s_o_n_b_path_exists一个任何地方都不存在的名字。sql_names()对运算符没有函数拼写因此回退用 snake_case 化类名。这两半——静默缺口与不可执行的报错——都属于开放问题 1。可接受的分歧由谁编写了行为裁决。该面发明的概念要承担全部一致责任没人能拿规范校验它拒绝可恢复悄悄不同的数字不可恢复引擎语义不属于该面调和的范围强行调和只会更糟——让-两端一致要么覆盖数据库的文档化行为要么降级已做正确事情的端。执行信封在排序敏感操作使用未 CAST 的 JSON 提取时给出警告存在活动表达式索引时full 详细级别的 schema 还发布精确的索引拼写。因此非对称是一种决策并被记录为决策admission_corpus.py以每条语句按方言各带一份、附结果与原因的方式记录。未声明的非对称按定义是缺陷。决策白名单解析树而非语句文本文本级过滤会被注释、空白、大小写、unicode 与嵌套构造打败。先解析意味着策略与引擎读取同一构件因为引擎运行的语句正是从策略检查过的树打印的。代价是对 SQLGlot 解析忠实性的依赖而这个依赖比表面更重不忠实的解析不会使策略与引擎失步二者都源于树而保持一致而是使二者都与调用方失步——调用方的文本在阶段 1 就被丢弃、永不再查。下游无法察觉原因有二每个下游检查读的是同一棵错误之树管线中不存在第二个意见且在错误树上往返是稳定的——解析、渲染、再解析得到原样自洽性检查必然通过。由此衍生两条实践准入拒绝它不认识而非忽略的节点类admission_corpus.py记录每一个曾漏过的构造。决策重写语句而非拒绝需要重写的语句调用方要latency_ms是在问 schema 已广告的问题。拒绝并解释正确表达式要花一次往返还假设模型能写对替换只需一次交换。代价是执行语句可能变成调用方没问的东西。两个缓解措施针对各 pass替换表达式整体加括号绑定位置与列一致liveness 套件对种子行而非空表执行每个被允许的构造。但两者都够不到含义变化的另一来源——解析本身一棵已歪曲调用方语句的树会被忠实地重写成一棵同样歪曲的语句且上述两个缓解都会报告成功。决策schema 是 DDL而非 JSONdescribeSqlSchema在brief级别返回纯注释的表目录在detailed与full级别选择请求方言的打包CREATE TABLE语句并追加--策展注释。PostgreSQL 的public.限定从CREATE TABLE与REFERENCES语法中移除调用方 SQL 必须使用未限定表名物理语句其余部分原样保留。三个理由省 token已实测比等价 JSON 目录少约三分之一 token——JSON 为每列重复name/type/nullable键。数字随渲染器获得约束与列注释而变化当前数值由ddl.py承载spec 文档刻意不复述因为同一测量的两份拷贝会漂移。结构性JSON 类型是抽象DDL 不是。方言资产直接报告start_time在 SQLite 是TIMESTAMP、在 PostgreSQL 是TIMESTAMP WITH TIME ZONE——写比较或 CAST 的调用方必须知道这个。原生性它就是调用方写回的形态。物理 DDL 无法提供的策展——区域、grain行粒度、虚拟列声明、时间列标签、提升列指引与语义列注释——以注释形式随行。虚拟列是注释而非物理CREATE TABLE的成员重写使它们在查询中行为如列。原始外键被保留因此被选表可能描述指向分析白名单外表的引用该目标是可用的物理上下文而非查询许可——前置声明中写明全局白名单定义可查询表准入会拒绝该目标。返回前渲染文本要过解析因为看起来像 DDL 的文本仍可能无效而调用方无法分辨对他们是散文。决策schema 是文本结果是 dict两个工具返回不同形态因为消费方式不同。describeSqlSchema返回散文——没人解析它JSON 包装不增加读者使用的结构反而转义一个几乎全是换行的文档的每个换行detailed级别约 174 token、约 7%output_schemaNone还抑制结构化镜像——对散文而言那是文本块的逐字重复。executeSql返回 dict结果集以数据而非待解析文本到达。关于重复MCP 的CallToolResult携带必需的content列表与可选的structuredContent二者都发出是惯例——文本块供所有客户端可读结构化视图供理解它的客户端使用。这不是浪费而且在该面实际驱动的路径上见消费模型完全不花成本。决策结果信封只携带会变化的内容凡不可能取第二个值的字段都被移除。拆分前一行结果 696 字节中 53 字节是行、401 字节不可能不同一个硬编码报告所有区域可用的availability图、字面量read_only: true、字节上限、以及每次调用逐字重复的consistency注释。这些是面的属性因此改由describeSqlSchema携带——每次调用该工具一次而调用方调用它的频率远低于运行查询。结果信封结构见 output.pycolumns、rows、row_count、row_count_is_partial截断的权威答案因为多取了一行、applied生效方言、钳制后 limit、触发的重写、必要时含实际执行 SQL、backend_validated、notes与 PostgreSQL 独有的estimated_rows规划器对未截断行数的估计绝不回答截断问题。决策白名单表的每个物理列都可查询白名单策展的是表而非列。表一旦准入其方言 DDL 资产中的每个物理列都被广告、被准入接受、被纳入SELECT *适用虚拟覆盖层追加其后。显示属性与指向非白名单表的外键因此作为值可见尽管被引用表本身仍不可查询。这使 schema 教学、准入与星号展开共用同一目录。未知基表列失败关闭而迁移新增的物理列在重新生成资产落地时被有意加入面。真正在面之外的东西必须住在未白名单的表中。决策不注入默认时间窗口早期版本在调用方未给窗口时注入尾随七天窗口。它无法约束坚决的调用方破解它只需一个参数对其他人则回答了未问的问题还报告成功。约二十五次冷启动 Agent 运行中每个调用方都注意到并绕开了它因此它保护不了任何人还让每个人都付出一次往返。行数与字节上限约束答案截止时间约束工作量。决策读路径避开写入方两条不同路线Phoenix 增加了专用 SQLite 读引擎modero、队列池使读不再排队在写入方单一StaticPool连接之后。实测NullPool490 条/秒对比队列池 3,879 条/秒这正是选池而非每次读建连接的原因。该面在 PostgreSQL 执行路径与目录读取索引反射、引擎版本、schema 解析中通过db.read()使用它。SQLite 的executeSql刻意不用它每次语句自行打开sqlean.connect(...?modero)因为约束查询的 authorizer 回调与 progress handler 是每连接态的不能活得比语句更久——在池化连接上下一个调用方会继承它们或在查询中途被剥离modero也在打开时固定事后无法强加。放弃池并无损失因为 SQLite 执行宽度是 1——无论如何一次只跑一条语句。读己之写read-your-writes在池化路径上不保证DbSessionFactory.read上已声明——此前对 PostgreSQL 副本本就不保证。决策物理 schema 来自打包 DDL策展来自类型化 Pythonsrc/phoenix/db/ddl/ 下的生成式 PostgreSQL 与 SQLite schema 资产随 Phoenix 打包是物理CREATE TABLE文本与有序列名的运行时来源。加载器按确定性-- Table: name段落索引要求恰好一个匹配的CREATE TABLE不经 SQLGlot 往返提取列保留带引号标识符语义返回不可变按方言目录。加载分析白名单时若白名单表缺失或虚拟列与物理列大小写不敏感地碰撞则加载失败。生成器仍在 scripts/ddl/ 下。make schema-ddl重新生成两份规范资产、校验加载器可消费它们、比较两方言的表/有序列/显式索引/命名约束漂移CI 运行该目标并要求干净的 Git diff因此改变物理 schema 的迁移必须更新已检入资产。索引不同full详细级别从pg_get_indexdef或sqlite_master.sql实时读取因为表达式索引定义是部署事实调用方必须逐字复现其拼写brief与detailed不读实时目录。不可变 manifest.py 模块只提供策略与策展表/区域白名单、grain、时间列标签、虚拟列声明、提升列指引与语义列注释graphql_node_id的适用性还由代码中 GraphQL 类型映射派生见 allowlist.py 的TABLE_GRAPHQL_TYPES。原始物理外键即使指向非白名单表也保留在 DDL 中准入仍执行全局表白名单。PostgreSQL schema 针对连接解析而非假设环境变量设置时用之否则用未限定projects引用解析出的 schema而非current_schema()——它报告CREATE会落在哪里而非表所在哪里两者在迁移后search_path头部新增条目时即分叉。执行与 full 详细级别的索引反射用同一解析因此为表发布的索引属于查询所读的关系。当前三个区域与表的划分manifest.pytelemetryprojects、tracesstart_time为时间列虚拟列latency_ms、spansgrain一条 OpenTelemetry span含parent_id/span_id/trace_rowid的列注释与llm_token_count_*提升列指引、span_annotations、span_costs、span_cost_details、generative_models、project_sessionsdatasetsdatasets、dataset_versions、dataset_examples、dataset_example_revisionsgrain数据集示例的一条不可变修订experimentsexperiments、experiments_dataset_examples、experiment_runsstart_time时间列虚拟列latency_ms、experiment_run_annotations。策展字段保持手写。time_column是渲染为注释的教学标签不注入或改变查询窗口grain、提升列指引与列注释同样只影响 schema 教学。十、消费模型token 核算该面的 token 核算取决于客户端如何调用它而默认不是显而易见的那条。在MCP 代码模式默认下模型不接收工具结果它写 Python 调用call_tool(...)后者返回反序列化的 dict只有该代码返回的内容进入模型上下文。中间结果在沙箱内被过滤与聚合。五次编排的executeSql调用实测沙箱内取回的信封共 11,522 字节到达模型上下文的约 200 字节。因此content/structuredContent的重复在这里不可见——代码两者都不看只看 dict。每次调用的信封大小远不如每个结果都被上浮时重要——这正是executeSql返回结构化数据而非文本的原因也是值得裁剪信封的原因因为确实上浮结果的调用方要为每个字段付费。对于直接渲染每个工具结果的客户端两半都被计费这正是describeSqlSchema上output_schemaNone针对的情形。十一、测试策略该面占主导地位的缺陷类别是多个策略层之间夹着一个变换器各自单独验证、从不相互对验。因此套件测试的是目录、策略、重写与引擎背封之间的一致性而非只隔离测试每个组件。测试位于 tests/unit/server/mcp/sql/准入语料admission_corpus.py——每个曾漏过的构造一条记录连同其现在必须产生的结果。削弱白名单最廉价的方式是在测试仍通过时把它加宽。Liveness——每个被允许的构造对种子行执行并必须返回它们。对空表执行只能验证解析器与 authorizer 一致无法验证构造是否真的算得出东西。节点覆盖——钉住可达的非Func表达式类因为多个绕过来自表里一套实际另一套的类。DDL 资产——加载器测试覆盖标记分隔段落、精确CREATE TABLE提取、有序与带引号列、不可变性、以及排除后续索引语句。make schema-ddl校验两份生成资产、比较其逻辑形态CI 拒绝任何重新生成 diff。文档 vs 执行器——对每张表比较SELECT *展开与该方言资产的有序物理列加适用虚拟覆盖层代表性物理列含显示与外键字段经准入提交未知名称必须带实用建议被拒。渲染 DDL 可解析——每个详细级别与方言在返回前被解析。测试另钉住 detailed 输出以所选物理资产开头、虚拟覆盖层保持注释、原始外键可描述非白名单目标。仍未自动化的部分把每个广告列、外键与CHECK字面量都提交给执行器。full 详细级别索引是实时部署数据因此测试改为钉住发布其表达式所用的解析器 workaround。读者不应假设 CI 会抓住每个未来的文档 vs 引擎失配。另有专门的 test_percentile_parity.py、test_plan_gate.py、test_sqlite_authorizer.py 与 test_liveness.py 覆盖各背封与跨后端一致性。亲眼运行以下两段可直接整段粘贴进 MCP Inspector。该面以代码模式驱动因此一次调用是返回工具反序列化 dict 的 Python而非待填写的表单。describeSqlSchema的full详细级别是值得一做的调用因为full是唯一读取实时目录的级别。其表 DDL 仍来自打包方言资产实时读取提供部署的索引return await call_tool( describeSqlSchema, { tables: [spans], detail: full, }, )它返回带策展注释的spansDDL、编写 JSON 运算符的前置规则以及渲染为CREATE INDEX的部署表达式索引。索引段落是值得细读的部分表达式索引只有在查询逐字复现其表达式时才可用因此这些拼写是要求而非提示没见过它们的调用方猜不出多个等价形式中哪个被索引了。executeSql随后可以问一个固定 API 无法回答的问题因为其维度无法预知——按模型的尾延迟其中模型名是 JSON 路径而非列return await call_tool( executeSql, { sql: SELECT attributes # {llm,model_name} AS model, count(*) AS calls, round(percentile_cont(0.95) WITHIN GROUP (ORDER BY latency_ms)::numeric, 1) AS p95_ms FROM spans WHERE attributes ? llm GROUP BY model HAVING count(*) 5 ORDER BY p95_ms DESC }, )该语句中有四处值得注意每处都是本文记录过的一个决策attributes # {llm,model_name}是describeSqlSchema为索引路径发布的拼写无需 CAST路径字面量无需转型attributes ? llm是键存在测试PostgreSQL 准入、SQLite 缺失是声明的非对称而非缺口percentile_cont(...) WITHIN GROUP在此准入、SQLite 拒绝并给出命名percentile(x, p)的消息latency_ms根本不是列。最后一点在答案中可见信封报告applied.rewrites为latency_ms、schema_qualification、limit_injection——虚拟列被替换为按方言表达式、spans针对连接解析、row_limit 1被追加使截断可检测。十二、开放问题按它们会改变设计多少排序一个策略、十处枚举、四套词汇。允许哪些计算运行写在十处分别表达为 SQLGlot 类、调用方拼写、SQLite authorizer 名称与 PostgreSQL 计划标识符。没有一集合由另一集合派生一致性靠测试维持四处分歧记录在代码注释中。生成式能力表会让缺失的单元在导入时失败。结果未被标记为不可信内容尽管它们携带受攻击者影响的文本进入持有破坏性工具的模型。SQLGlot 把 PostgreSQL JSON 运算符解析为访问器而非二元运算符。-、-、#、#与?是 SQLGlotCOLUMN_OPERATORS的条目以最紧优先级结合右操作数解析不一致PostgreSQL 把它们放在任意其他运算符层级低于算术、与||同级。四组分群出错输入SQLGlot 构建PostgreSQL 含义a # b::text[]CAST(a # b AS TEXT[])a # CAST(b AS TEXT[])a - b[1](a - b)[1]a - b[1]a - b.c - da - (b.c - d)(a - b.c) - da - b 1(a - b) 1a - (b 1)缺陷在解析中下游检查无法恢复含义且往返不暴露它——解析、渲染、再解析返回同一棵错误之树。第二、四行渲染回调用方逐字文本PostgreSQL 按其自身优先级重解析含义靠偶然而非设计存活第一、三行把错误分组渲染进CAST或JSON_EXTRACT_PATH调用任何引擎都无法重新解释。括号击败全部四例带括号的操作数落在Paren节点下、作为整体结合a # ({a,b}::text[])、a - (a[1])、a - (b.c) - d、a - (1 1)都按 PostgreSQL 的读法解析与渲染四例均对 PostgreSQL 17 验证过。三个缓解措施已就位catalog.py从该面发布的索引拼写中丢弃冗余 CASTpg_get_indexdef对 JSON 路径表达式索引发出{a,b}::text[]剥离形式到达同一索引用EXPLAIN验证schema 前置声明开篇即写明括号规则让调用方在写 SQL 前而非被拒后学会它准入直接拒绝第一行——因为该树也是故意的CAST(a # b AS text[])所产生的两种读法解析后无法区分静默任选其一都会回答未问的问题故拒绝消息点名两个无歧义拼写。剩余未覆盖的是不遵守前置声明的前两行之外的调用方第二、四行仍渲染为调用方原文PostgreSQL 恢复含义第三行不会——a - b.c - d渲染为JSON_EXTRACT_PATH调用且结合性已错向固定返回错误值且无任何报告。在 sqlglot 固定为 30.14.0/30.15.0 期间重关联第三行曾被调研并否决其形态与第一行同样歧义只能靠未文档化的解析器内部实现区分基于此的含义改变型重写是坏交易。该问题已上报上游并在 sqlglot 30.16.0 修复——JSON 运算符移到 Postgres 二元运算符优先级层该版本上每行以及col - k都以中缀往返。Phoenix 当前固定sqlglot30.15.0缓解措施保留检索WORKAROUND sqlglot30.15.0定位站点固定版本越过 30.16.0 后移除。准入对a # b::text[]的拒绝届时可移除因为修正后的解析器能区分它存在的两种读法。白名单上的jsonb_extract_path不是 workaround——调用方会直接写它。计划闸门与 SQLite authorizer 不是等价背封见阶段 6。十三、相关文档设计规格internal_docs/specs/mcp-analytics-sql.md实现目录src/phoenix/server/mcp/sql/入口 tools.py核心执行 execute.py重写 rewrite.py准入 parse.py函数策略 allowlist.py策展 manifest.pyschema 渲染 ddl.py结果信封 output.py打包 DDL 资产src/phoenix/db/ddl/postgresql_schema.sql、src/phoenix/db/ddl/sqlite_schema.sql加载器 src/phoenix/db/ddl/loader.py测试tests/unit/server/mcp/sql/关联设计只读副本路由赞分享可观测性AI 评测LLMOpsAI 应用人工智能【免费下载链接】phoenixAI Observability Evaluation项目地址https://gitcode.com/gh_mirrors/phoenix13/phoenix点击查看免费下载相关推荐Granite-20B-Code-Base-8K vs 其他代码模型谁才是开发者真正的生产力工具Granite 20B Code Base 8K vs 其他代码模型谁才是开发者真正的生产力工具 Granite 20B Code Base 8K 是一款专为Lightdash 的 Effective dbt SQL 实战指南面向 Agent 与开发者的模型 SQL 语义规范Lightdash 的 Effective dbt SQL 实战指南面向 Agent 与开发者的模型 SQL 语义规范 本文基于 Lightdash 仓库 s后端前端数据分析数据可视化人工智能AI AgentNocoBase SQL 表SQL Collection用 SQL 查询构建只读报表数据表的完整实践NocoBase SQL 表SQL Collection用 SQL 查询构建只读报表数据表的完整实践 本文基于 NocoBase 官方文档《SQL 表》与低代码后端前端人工智能AI 应用工作流自动化上一篇nerfstudio LocalWriter 终端日志指南配置、输出格式与自定义统计项下一篇Arduino ESP32完整开发指南从零开始构建物联网应用创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考