开场不要念标题。第一句话说清这份材料要回答的那个具体问题:「当 agent 要分析 S3 上的数据,把引擎放在远端还是放在进程内?」
可能被追问:「这不就是把 DuckDB 塞进 Lambda 吗?」→ 差别在治理层和四条访问路径,这两块是这个实现里真正难的部分,第 Ⅱ、Ⅲ 章各占一半时间。先看议程。
议程只花 40 秒。要说的一句是:「四章是推导顺序,不是知识分类」—— 第 Ⅲ 章之所以存在,是因为第 Ⅰ 章的结论(引擎必须在进程内)直接引出了一个新问题:进程内的第三方引擎拿不到 Lake Formation 的行列过滤。
_conn_lock 那页值得多留 2 分钟,那是个通用陷阱,不只影响 DuckDB。可能被追问:「能不能只讲治理」→ 可以,但先给 3 分钟看第 Ⅰ 章的 A/B 表,否则「为什么不用 Athena + Lake Formation」这个问题会一直悬着。现在进第 Ⅰ 章。
转场 20 秒。一句话:「这一章不讲 DuckDB 有多快,讲的是 agent 的失败次数怎么定价。」
先看现象 —— 一个自然语言问题在引擎侧到底变成了什么。
SELECT txid, fee, output_value … LIMIT 5sum(total_fee),引擎报错。这一步是必然会发生的count(*) + sum(fee)approx_quantile(fee, 0.5/0.99)ORDER BY fee DESC LIMIT 10这页要说服的是:「不要用『一条查询多快』来评估 agent 的数据后端,要用『一个问题多快』。」人写 SQL 一次写对,agent 是靠读错误信息收敛的。
s3://aws-public-blockchain 的 2026-08-25 分区)、同一批文件;DuckDB 侧是热进程的 S3 range read,Athena 侧是完整的 StartQueryExecution→poll 生命周期,也就是 MCP 工具每条查询实际要付的那一段。可能被追问:「Athena 有结果缓存、也可以并发提交,为什么不算进去」→ 结果缓存对 agent 帮助有限,因为它每一条 SQL 都是新拼的;并发提交解决不了「必须读上一条的结果才能写下一条」这个依赖链。下一页把这个现象抽象成四层架构。
这页的论点是一个次序问题:「把引擎放进进程」之所以成立,前提是另外三层已经搬出去了。顺序颠倒就会得到一个很糟的架构。
duckdb.connect() 不带路径(纯内存);治理外置 = governance.py 独立模块 + GOVERNANCE_POLICIES 环境变量;调度外置 = 完全不写调度代码,靠 AgentCore 一会话一 microVM。可能被追问:「1TB 以内是不是太乐观」→ 这个模式的适用边界在最后的决策表里有明确一行:工作集接近单节点内存就该拆查询或上移到 Athena/Redshift。本项目实测过 256MB 信封 + 落盘,全天排序 2.5s 完成,是「更慢但不死」。下一页给证据。
scripts/ab_compare.py 生成 docs/ab-results.md · Athena 侧用分区投影建表,无 crawler| # | 查询 | DuckDB s | Athena s | Athena 排队 ms | Athena 扫描 MB | 成功 D/A |
|---|---|---|---|---|---|---|
| 1 | 探 schema | 1.39 | 1.84 | 68 | 2.0 | ✓ / ✓ |
| 2 | 写错列名(agent 的错) | 0.18 | 0.51 | 55 | 0.0 | ✗ / ✗ |
| 3 | 改正后的聚合 | 0.65 | 1.35 | 90 | 1.1 | ✓ / ✓ |
| 4 | 手续费分位数 | 0.45 | 1.78 | 56 | 1.1 | ✓ / ✓ |
| 5 | Top-10 by fee | 1.19 | 2.61 | 54 | 42.1 | ✓ / ✓ |
| 合计(一个问题的完整试错序列) | 3.9 | 8.1 | 323 | 46.2 | 2.1× 差距 |
StartQueryExecution→轮询的完整生命周期,也就是一个 MCP 工具每条查询真正要付的那一段;
② 计费模型不同:Athena 按扫描字节计费(本序列 46MB),DuckDB 的边际成本是 S3 GET;
③ 分位数函数方言不同(approx_quantile vs approx_percentile),第 4 行不是同一个实现,不要单独引用这一行。这是第 Ⅰ 章的收口证据。要主动说清它不是什么:「这不是说 DuckDB 比 Athena 快 2 倍。」Athena 在多 TB 扫描上会反超,那是它的设计目标。这张表说的是「agent 的试错序列」这一个特定负载。
可能被追问:「那为什么不用 Athena 的 API 直连、省掉 MCP」→ 排队和启动开销是服务侧的,不是客户端的,绕不开。真正能绕开的做法就是把引擎搬进来 —— 这就是第 Ⅱ 章要讲的:搬进来之后,数据怎么进这个引擎。
转场。一句话:「引擎不变,目录策略可以随治理需求演进。」这是这一章存在的全部理由 —— 四条路径不是四个方案让你挑一个,而是同一套代码在不同治理成熟度下的四种落法。
下一页是这份材料唯一的一张全景图,也是仓库里那张会动的架构图。
这张图有三个刻意的设计,投屏时按这个顺序指:①「AgentCore Identity 在 microVM 外面」—— 认证发生在 agent 代码运行之前,不是 agent 的职责;②「治理框在 DuckDB 上面」—— 引擎永远看不到未治理的 SQL;③「四条路径是同一区域里的平级框」—— 没有主次。
docs/architecture.gen.py 是唯一权威,SVG 是产物 —— 改结构要改 .py 再重新渲染,不要手改 SVG。这也是为什么图里的连线严格正交、标签不打架。可能被追问:「Bedrock 为什么画在 microVM 外面」→ 因为它是 agent 调出去的外部服务,不在会话进程里;把它画进去会让人误以为模型在本地。下一页把这张图的流程展开成五步。
用户在 IdP 登录;AgentCore Identity 在服务入口校验 JWT,只有已验证请求能进会话
会话入口把已验证的 {tenant, role} 映射到角色策略:身份在 IdP,策略在配置
agent 调 Bedrock(Claude):DESCRIBE、采样、读错误、改 SQL、聚合 —— 引擎反馈毫秒回来
run_sql 按角色策略重写语句:RLS 谓词、列排除、原始路径拒绝,然后进程内 DuckDB 执行
A 路径 glob · B S3 Tables Iceberg REST · C Glue Iceberg REST · D DuckLake SQL 目录。数据始终留在 S3
| 步骤 | 责任方 | 失败时的行为 |
|---|---|---|
| 1 认证 | AgentCore Identity | 请求根本到不了 agent 代码 |
| 2 声明→角色 | agent.py 入口 | 回落到默认角色(可配置) |
| 3 模型循环 | Bedrock + Strands | 引擎错误原样回给模型,自修复 |
| 4 重写 | governance.py | 解析不了就拒绝执行(fail closed) |
| 5 取数 | DuckDB 扩展 | ATTACH 失败则该条腿不可用,其余不受影响 |
这页是上一张图的文字版,价值在右边那张「为什么必须是这个次序」。要说服的一句是:「我们敢让模型自由生成 SQL,是因为它生成的每一条都还要过第 4 步。」
read_parquet('s3://…') 想绕过表级策略时,第 4 步会直接拒绝 —— 第 Ⅲ 章有实跑的拒绝信息。build_catalog_doc(active_legs()) 只把真正挂上的腿写进系统提示词 —— 没挂上的腿绝不能泄漏到提示词里,否则模型会去查一个不存在的目录。可能被追问:「第 2 步回落到默认角色,是不是等于绕过治理」→ 不是。默认角色由 GOVERNANCE_DEFAULT_ROLE 指定,部署时可以把它设成最严格的那个角色。第 Ⅲ 章会讲这个两阶段映射。下一页先把四条路径摆在一起对比。
| 维度 | A · 原始 Parquet | B · S3 Tables | C · Glue 目录 | D · DuckLake |
|---|---|---|---|---|
| 库内别名 | 视图 / read_parquet | s3t.* | glue.* | dl.* |
| 解析方式 | S3 路径 glob + LIST | Iceberg REST(S3 Tables 原生端点) | Iceberg REST(Glue 端点,联邦 s3tablescatalog) | SQL 目录库一次查询 → Parquet range GET |
| 需要 S3 LIST | 全历史 glob 要列 ~6000 分区 | 零 LIST,指针链导航 | ||
| 元数据归属 | 无 —— 文件即真相 | S3 Tables 服务持有当前版本指针 | Glue / Lake Formation | 一个 catalog 数据库持有全部快照与文件清单 |
| 能与谁共享 | 仅本引擎 | 引擎直连、纯 IAM | Athena / Redshift 全组织可见 | 接得上 DuckLake 的引擎 |
| 额外基础设施 | 零 | 表桶(托管) | 联邦目录,IAM 模式 | 单文件目录=零;多写入需 PG |
| 环境变量开关 | 恒开 | S3_TABLES_ARN | GLUE_CATALOG | DUCKLAKE_CATALOG |
| 实测 engine_ms(同一天切片聚合) | 1.3s | 1.6s | 1.8s | 3.5s |
| 怎么选:历史数据与临时文件走 A(无需任何 setup);需要 ACID / 压实 / 快照就默认从 B 起步(本表主推列); 要让 Athena、Redshift 看到同一批表就加 C —— C 和 B 是同一张物理表,只是换了个端点解析,不是副本;元数据极多(大量小表 / 频繁快照)时 D 的单次 SQL 解析更划算。 | ||||
engine_ms 是 2026-08-31 在同一个云上会话里连续跑完四条得到的 ——
进程预热状态一致,所以四个数彼此可比;但不要拿它们跟别处的绝对值比。
② 四条腿全开的成本只发生在会话建立时各一次 ATTACH,不查询就不产生数据面费用 ——
所以「全开」是本项目做一致性验收的手段,不是推荐的生产配置。不要逐行念。指着 tfoot 说「结论在这」,然后只展开两行:「需要 S3 LIST」和「能与谁共享」。这两行是真正的分界线,其余都是它们的推论。
glue:Get*(限定到联邦目录路径)和 lakeformation:GetDataAccess。可能被追问:「四条都开销不开销」→ 开销只在 ATTACH 时一次性发生,不查就不花钱;本项目默认全开是为了做一致性验收。下一页单独讲 D,因为它有一个真实的选型教训。
DUCKLAKE_CATALOG=s3://…/blockchain.ducklakeDUCKLAKE_CATALOG=postgres:dbname=…,凭据从 Secrets Manager 取(绝不走环境变量)。
适用于摄取管道在提交快照、agent 同时在读的多写入库。代价:runtime 必须切 VPC 模式才能到 RDS,
且 microVM 连接短命而多,建议加 RDS Proxy。| 切 VPC 模式会连带拖进来什么 | 实际发生的事 |
|---|---|
agentcore configure --vpc | 打印成功,但不会真的写进 yaml —— 必须手改 network_configuration |
| bedrock-runtime 私有 DNS | 接口端点的私有 DNS 会覆盖 NAT 路径;安全组不放行 runtime SG 的 443 → invoke 直接 500「Connect timeout」 |
| 退回 PUBLIC 之后 | 删 runtime 安全组要等服务侧回收 ENI,不能立即删 |
| 结论 | RDS/VPC 这条腿建过、验证过、又拆掉了(2026-08-31);代码保留,默认不启用 |
这页讲的是一个被撤销的决定,比讲一个成功的决定有用。要说的一句是:「多写入能力不是免费的,它的价格是一个常驻组件加一整套 VPC 网络。」
可能被追问:「那多写入到底怎么办」→ 两条路:真的需要就切 postgres 形态并加 RDS Proxy(代码已支持);或者让摄取侧写 Iceberg(Path B)而 agent 读 Path B —— S3 Tables 服务本身就在管当前版本指针,天生支持写读并发。下一页给四条路径的一致性证据。
每次 run_sql 返回都带第二行 {"engine_ms", "rows", "governed"},
系统提示词要求模型在总结里引用 engine_ms —— 所以这些数字是模型自己报出来的,不是我另外测的。
count(*) 返回 0 行,本地怎么跑都正常_conn_lock 串行化 run_sql;回归测试钉住细节在第 Ⅳ 章有一整页 —— 这里只说明:四路径验收的价值不只是「结果对」, 而是它把一个只在云上出现的并发缺陷逼了出来。
这页的说服力在「逐位相同」。3.14201333 BTC 这个数四条路径完全一致,说明四条腿读到的是同一份事实,不是四份近似。这是「换目录策略不影响正确性」这句话的唯一证据形式。
可能被追问:「620,604 行为什么不是 5.3M」→ 5.3M 是各条腿装载的整个切片(8 月 20–28 日九天),620,604 是验收那条查询针对的单天。两个数不是一回事,别混。下一章讲:这四条路径上的行列级权限怎么管。
转场。这一章是全篇技术密度最高的一章,开场就把结论说了:「把引擎搬进进程,代价是失去了引擎自带的行列级治理。这一章讲怎么把它补回来,而且补得比原来更可移植。」
先把边界切清楚,否则后面讲重写会一直有人问「那认证呢」。
X-Amzn-…-Runtime-User-Id 头 —— SigV4 路径。调用方应用已经认证过它自己的终端用户,
按 <tenant>:<role> 约定传入identity 对象 —— 供内联传租户/角色的调用方使用。
SigV4 调用方是 IAM 认证过的基础设施,被信任来断言其用户身份 —— 与 ② 同一个信任模型GOVERNANCE_DEFAULT_ROLE。部署时应把它设成最严格的角色IdP 验证过的声明 → tenant:role 键。未知角色回落到该租户最严格的策略,不是回落到 admin
行过滤谓词 + 列拒绝清单 + allow_raw_paths 开关。来自 GOVERNANCE_POLICIES JSON 环境变量
为什么要两阶段:IdP 里的人员进出、组织调整每天都在发生; 策略表里只该有角色,不该有人。策略是配置,不是代码 —— 改策略不需要改代码、不需要重新部署镜像。 规模再大就把这张表搬到 DynamoDB,resolve / rewrite 的契约不变。
governance.py 里没有一行验签代码,
agent.py 里没有一行策略判断。认证归 AgentCore Identity,授权归重写层 ——
这条切分要在 code review 里守住,一旦某一侧开始「顺手也做一点」另一侧的事,信任模型就再也说不清了。这页要说服的是:「不要在 agent 里做认证。」agent 代码只消费「已验证的声明」这一个窄契约,这样它既不需要知道 IdP 是谁,也不需要保管任何验签密钥。
tenant:role 精确命中优先,没命中就取该 tenant 的第一条策略,都没有才用默认角色。可能被追问:「同一个进程里会不会串号」→ 不会,一个会话 = 一个 microVM = 一个主体,所以活动角色是模块级槽位,这是刻意的设计而不是偷懒;但它确实意味着不能在一个会话里服务多个用户。下一页看重写具体做了什么。
-- 主体 cex-a:analyst 问「2026-08-25 有多少笔、最大手续费多少」 -- ① 模型写出的 SQL(进入 governance 之前) SELECT count(*) AS n, max(fee) FROM btc_transactions WHERE date='2026-08-25' -- ② apply_governance() 的输出(引擎唯一看到的东西) SELECT COUNT(*) AS n, MAX(fee) FROM ( SELECT * EXCLUDE (fee) -- CLS 列级 FROM btc_transactions WHERE date >= '2026-08-24' -- RLS 行级 ) AS btc_transactions WHERE date = '2026-08-25' -- ③ 换任意一条访问路径,策略照样命中(按裸表名匹配) SELECT t.txid FROM ( SELECT * EXCLUDE (fee) FROM s3t.blockchain.btc_transactions WHERE date >= '2026-08-24' ) AS t JOIN eth_blocks AS b ON t.block_number = b.number -- 未受治理的表原样保留
(SELECT * EXCLUDE(…) FROM ref WHERE …) AS alias,别名保留,所以 JOIN 里的 t.txid 不会断raw、s3t.*、glue.*、dl.*
四条路径同一条策略,加新路径不用改策略WITH w AS (SELECT * FROM btc_transactions) … FROM w 会被套两层read_parquet/read_csv/read_json 直接拒绝(否则路径访问就整体绕过了表级策略);
解析不了的 SQL 也拒绝 —— 不是「放过去让引擎自己判断」。这是全篇最该慢下来的一页。指着高亮那三行说:「这就是全部。行级是一个 WHERE,列级是一个 EXCLUDE,包在一个子查询里。」没有自研 SQL 引擎,没有 AST 魔法,就是生产 BI 引擎注入行级安全的那套老办法。
s3t.blockchain.btc_transactions 照样命中,而同一句 SQL 里未受治理的 eth_blocks 原样不动。这说明策略是按逻辑表管理的,不是按物理路径管理的 —— 加第五条访问路径时策略表一行都不用改。SELECT fee 时,重写后的语句里 fee 已经被 EXCLUDE 掉,引擎会报 binder 错误。这比返回 NULL 好得多 —— NULL 会让模型以为「这天没有手续费数据」,从而给出错误结论;报错会让模型如实说「这个列不可见」。可能被追问:「模型能不能用 SELECT * FROM (SELECT fee AS f …) 绕过」→ 不能,重写是在 AST 上做的,任何层级的表引用都会被包裹。真正的绕过入口是原始文件路径,所以那条被单独 fail closed 掉了。下一页讲这套重写里最难的一处。
read_parquet 的 glob
取第一个文件的 schema —— 2009 年的比特币文件没有 fee 列EXCLUDE 一个关系并不暴露的列,是
binder 错误,不是无操作deny_columns:["fee"],
走 s3t.* 正常工作,走原始视图直接把查询打挂try: EXCLUDE except: 不 EXCLUDE。那就是静默 fail open ——
列级策略在某些路径上无声失效,而日志里看不出任何区别。agent 侧把 DESCRIBE SELECT * FROM <ref> LIMIT 0 注册给 governance,按引用字符串缓存
拒绝清单 ∩ 该引用实际暴露的列。关系里根本没有的列本来就取不到,跳过它不会放宽访问
最坏情况是引擎拒绝这条查询。fail closed,永不 fail open
一个连带的并发细节:重写发生在
run_sql 的锁之外,所以探针必须自己去取 _conn_lock——
否则它的 DESCRIBE 会和正在执行的查询抢同一个结果集。这行注释在代码里是留给下一个人的。
这页是整个实现里我最想让人带走的一页。要说服的一句:「降级可以,但降级必须留痕;『检查失败』和『检查通过』在行为上必须长得不一样。」
btc_transactions,但这个逻辑表通过四条路径暴露出不同的列集合。原始视图那条腿因为 glob 绑定了 2009 年的 schema,根本没有 fee 这一列 —— 而 EXCLUDE 一个不存在的列在 DuckDB 里是硬错误。可能被追问:「为什么不干脆统一四条路径的 schema」→ 可以,但那就要求原始视图也只覆盖近期切片,而原始路径的价值恰恰是「能查 2009 年至今的全历史」。保留 schema 差异、让治理层去适配,比削掉能力更合理。下一页回答那个一直悬着的问题。
glue:Get*(限定到联邦目录路径)+ lakeformation:GetDataAccess| 维度 | ops:admin | cex-a:analyst |
|---|---|---|
| 返回行数 | 5,326,646 | 2,865,507 RLS 窗口 |
| max(fee) | 0.37090449 | 列不存在 模型如实报告 |
原始 read_parquet | 允许 | 被拒 附可解释的错误 |
governed 标记 | false | true |
| 响应信封 | principal 原样回显,便于审计 | |
策略来自 GOVERNANCE_POLICIES:ops:admin 不受限;
cex-a:analyst = 行窗口 date >= '2026-08-24'、拒 fee、禁原始路径。
改策略不改代码。
这页要避免一个常见的表达错误。不要说「LF 不好用」,要说:「LF 的行列过滤在它集成的引擎里工作得很好,而我们这个引擎不在那个名单里 —— 所以我们要在它管不到的那一段自己补。」这个措辞差别决定了客户会不会觉得你在贬低 AWS 自己的服务。
可能被追问:「审计怎么办」→ 目前是 governed 标记进工具返回、principal 进响应信封、日志进 CloudWatch。README 明确写了这是参考实现,生产前要补细粒度审计留痕、限流、告警。下一章讲那些真的摔过的地方。
转场。一句话:「前三章讲的是设计对不对,这一章讲的是实现会在哪里摔。」这一章对准备自己搭一套的人价值最高。
_conn_lock)是最通用的一条 —— 它跟 DuckDB 关系不大,跟「agent 框架把工具调用派到线程池」关系很大,任何持有单连接资源的工具都会撞上。先看四条腿是怎么挂上来的 —— 一共不到 30 行。
# Path B — S3 Tables 原生 Iceberg REST 端点 c.execute("INSTALL iceberg; LOAD iceberg;") c.execute(f"ATTACH '{S3_TABLES_ARN}' AS s3t " "(TYPE iceberg, ENDPOINT_TYPE s3_tables, READ_ONLY)") # Path C — 同一张物理表,换成 Glue 端点(联邦 s3tablescatalog) c.execute(f"ATTACH '{GLUE_CATALOG}' AS glue " "(TYPE iceberg, ENDPOINT_TYPE glue, READ_ONLY)") # Path D — DuckLake:SQL 目录 + S3 上的 Parquet c.execute("INSTALL ducklake; LOAD ducklake;") c.execute(f"ATTACH 'ducklake:{DUCKLAKE_CATALOG}' AS dl (READ_ONLY)") # 两个 S3 secret:区域不同,靠 SCOPE 区分 # 默认 us-west-2 → 签目录腿(托管存储 / ducklake 桶) # SCOPE 到公共桶 → us-east-2,签公共数据集 c.execute("CREATE OR REPLACE SECRET blockchain (TYPE s3, " "PROVIDER credential_chain, REGION 'us-east-2', " "SCOPE 's3://aws-public-blockchain')")
SCOPE 把公共桶单独绑到 us-east-2,不加 SCOPE 就会用错区域签名SET home_directory='/tmp' —— 容器 / SSM 里可能没有可解析的 HOME,
而 DuckDB 装扩展需要一个s3t / glue / dl。
别名会出现在系统提示词和每一条 SQL 里,短别名省 token 也少打错_attach_legs() 加挂载 → active_legs() 加开关 →
_LEG_DOCS / _SCENARIO_ROWS 加提示词条目。
治理策略一行都不用改(按裸表名匹配)。这页要传达的是「便宜」:四条访问路径的全部代价就是这不到 30 行,加上三个环境变量。指着高亮那行说「Path D 也就一句 ATTACH」。
s3://aws-public-blockchain 的 us-east-2 secret。少了这个 SCOPE,公共桶请求会用 us-west-2 签名而失败——注意失败信息不会告诉你是区域问题。PublicDatasetRead 语句就是为这个存在的。可能被追问:「READ_ONLY 是不是形式主义」→ 不是,单元测试里有一条真的对本地 ducklake 挂载后执行 INSERT 并断言抛错。下一页讲那道门。
# 只有这两类语句能过门 _READ_PREFIXES = ("select", "describe", "summarize", "with", "show", "explain") _TEMP_DDL = re.compile( r"^create\s+(?:or\s+replace\s+)?" r"temp(?:orary)?\s+(?:table|view)\s", re.IGNORECASE) def _gate(sql: str) -> str | None: normalized = " ".join(sql.strip().lower().split()) if normalized.startswith(_READ_PREFIXES): return None if _TEMP_DDL.match(normalized): return None # 会话内、随进程消亡 return "ERROR: only SELECT / … or CREATE TEMP TABLE|VIEW …" # 被门挡掉的(每一条都有测试):DROP / DELETE / INSERT / # UPDATE / 非 TEMP 的 CREATE / ATTACH / COPY TO / SET
CREATE TEMP TABLE
扫一遍 S3,后续所有追问都打在进程内表上,engine_ms 从秒级掉到毫秒级门禁 / 截断 / 指标 / 临时表跨调用可见性 / 路径开关 / 并发不偷结果 / 治理重写四条路径 / 别名保留 / CTE 与 CTAS 局部名 / 原始路径拒绝 / 不可解析拒绝 / 拒绝列不可达。目录腿逻辑用猴补环境变量 + 本地 ducklake 文件测,不碰网络。
DROP / DELETE / INSERT / UPDATE /
非 TEMP 的 CREATE / ATTACH / COPY TO / SET …)
把边界钉死。治理层才做 AST 级判断 —— 分工明确,各自都能被单独测试。这页要说的是一个判据设计问题:门必须简单到能被穷举测试。它不解析 SQL,只做归一化 + 前缀匹配 + 一个正则 —— 因为一个复杂的门本身就会成为漏洞来源。
test_parallel_run_sql_calls_do_not_steal_results,8 个线程各查一个不同的 count,断言每个都拿到自己的结果。这条测试是为下一页那个 bug 写的,它把一个云上才复现的缺陷钉在了本地。可能被追问:「前缀匹配会不会被注释绕过,比如 /*x*/DROP」→ 归一化只做小写和空白折叠,不去注释,所以这类语句前缀匹配不上、正则也匹配不上,会被拒绝——默认拒绝是对的方向。下一页讲那个最贵的 bug。
count(*) 「返回」0 行,本地任何跑法都正常。
成因:Strands 把同一条模型回复里的多个工具调用派到不同线程,
而 DuckDB 的 Python 连接只持有一个活动结果集 ——
两个未同步的 execute+fetch 配对互相偷走对方的行。threading.Lock 串行化 run_sql,
加一条回归测试(8 线程并发查不同 count,逐个断言拿到自己的结果)。
代码里写清了「为什么不能用另一种解法」,因为那才是下一个人会踩的地方。| 候选解法 | 能修好并发吗 | 为什么不能用 |
|---|---|---|
每线程一个 cursor() | 能 | 临时表会各有各的命名空间 —— 直接打破「会话状态」这个不变量:上一次调用建的 ws 下一次看不见 |
| 每次调用新建连接 | 能 | 同上,而且丢掉对象缓存、重复装扩展 |
| 一把连接级锁 | 能 | 唯一同时满足两个不变量的解法:结果集安全 + 临时表跨调用可见 |
| 禁止并行工具调用 | 能 | 要改框架行为,且白丢掉模型一次发多条查询的能力 |
| 连带的一处 | 治理的 schema 探针跑在 run_sql 的锁之外,所以它必须自己取这把锁 | |
这页是整章最通用的一条,跟 DuckDB 关系不大:「只要你的工具持有一个有状态的单例资源,而 agent 框架会把工具调用派到线程池,你就会撞上这个。」
可能被追问:「串行化会不会成为瓶颈」→ 在这个架构里不会:一个会话 = 一个 microVM = 一个用户,锁的竞争只发生在同一条模型回复里的几个工具调用之间,而它们本来就要按顺序读结果。并发能力靠开更多会话来的,不靠一个会话内部并行。下一页讲怎么把这套东西部署出来。
# scripts/load_s3tables.py —— 没有 Spark、没有 Glue job、没有 Athena CTAS c.execute(f"ATTACH '{arn}' AS s3t " "(TYPE iceberg, ENDPOINT_TYPE s3_tables)") c.execute(f"CREATE TABLE IF NOT EXISTS " f"s3t.blockchain.btc_transactions AS {source_select(days)}") # DuckLake 侧:能就从刚写好的 Iceberg 表拷(同区域,快) c.execute(f"ATTACH 'ducklake:{catalog}' AS dl (DATA_PATH '{data_path}')") c.execute("CREATE SCHEMA IF NOT EXISTS dl.blockchain") c.execute(f"CREATE TABLE IF NOT EXISTS dl.blockchain.btc_transactions AS {src}") # 幂等:IF NOT EXISTS + 行数已匹配就跳过全量重载 # 用你自己的凭据跑,不是 runtime 执行角色 —— 那个角色只读
| Sid | 给了什么 |
|---|---|
BedrockInvoke | 推理配置 + 基础模型 |
PublicDatasetRead | 公共数据集也要显式 allow(签名请求走授权路径,403 不是公共读) |
S3TablesRead | 7 个只读动作,限定到表桶及其 /table/* |
GlueFederatedRead | glue:Get* 限定到联邦目录路径 |
LakeFormationAccess | GetDataAccess(该动作不接受资源限定) |
agentcore launch 建——它还不是稳定的 CloudFormation 资源。
所以完整清理 = agentcore destroy + cdk destroy,
scripts/cleanup.sh 把两步按依赖顺序串好了。run_sql 门禁 + 四条腿 READ_ONLY 挂载)。
能力是有的,是被显式收走的;这比「它做不到」是完全不同的安全论证。这页有一个很好卖的点:「装数据的工具就是查询用的那个引擎。」指着高亮那两行 —— DuckDB 通过 S3 Tables 的 REST 目录自己写出了那张 Iceberg 表,5.3M 行,全程没有 Spark、没有 Glue job、没有 Athena CTAS。
可能被追问:「装数据用什么凭据」→ 用你自己的,不是 runtime 执行角色。那个角色是只读的,一次性写入不该给它写权限。最后一页把剩下的坑一次列完。
read_parquet('…/*/*.parquet') 取第一个文件的 schema,
而 2009 年的比特币文件没有 fee 列。演示与查询一律用显式单天路径或日期区间。
这条同时是 schema-aware CLS 存在的原因。
返回 MethodNotAllowed。这是设计如此,不是权限配错:
托管存储只允许按 key 做 GET,导航靠指针链走 —— 也正是它「零 S3 LIST」的来源。
别去修这个"错误"。
agentcore configure 不会把已存在的 agent 从
direct_code_deploy 切成 container。
手改 .bedrock_agentcore.yaml(deployment_type + ecr_repository),
让它和已部署的 runtime 对齐。
configure --vpc 打印成功但不写 yaml,得手改
network_configuration。若 invoke 报 500「Connect timeout on bedrock-runtime」:
接口端点的私有 DNS 覆盖了 NAT 路径,而它的安全组没放行 runtime SG 的 443。
并行 invoke 同一会话会拿到 Strands 的 ConcurrencyException。
做并发测试必须每个并发用新的 runtimeSessionId ——
这也正好对应「一会话一 microVM」的隔离模型。
DuckDB 装扩展需要一个 home 目录,而容器 / SSM 环境里未必有。
连接建立后第一件事就是 SET home_directory='/tmp'。
少了它,扩展安装会以一个和 HOME 毫无关系的信息失败。
不要六条都念,那要四分钟。做法:说一句「这六条都在材料里,会后自己看」,然后只展开 ② 和 ④——这两条最反直觉,也最耗时间。
可能被追问:「这些坑有没有可能被官方修掉」→ ③④ 属于工具链成熟度,会变;②⑥ 是设计使然,不会变;① 是数据集本身的 schema 演进,永远存在。下一页给决策表。
| 你的处境 | 建议 |
|---|---|
| 数据已经在受治理的数仓里,查询次数少但每条都重 | 直连数仓 MCP,跳过 DuckDB —— 这个模式帮不上忙 |
| 数据在 S3,agent 需要大量迭代,并发不可预测 | 用这个模式 —— 试错成本和爆炸半径都在进程内 |
| 需要按租户做行级 / 列级访问控制 | 用这个模式 —— 自带重写层。Lake Formation 的行列过滤只在它集成的引擎内生效 |
| 两种情况都有(多数真实场景) | 混合:DuckDB 快路径 + 数仓 MCP 兜底,按查询特征分流 |
| 工作集接近单节点内存上限 | 拆查询,或沿谱系上移(Athena / Redshift)—— 别硬撑 |
这页要先讲压暗的那两行(第一行和最后一行),也就是"别用"的两种情况。先说清什么时候不该用,后面的推荐才有人信。
.oc 必须念。把"这是参考实现、投产前还要补五项"说在前面,比让客户上线后自己发现要好得多。可能被追问:「混合方案的分流规则怎么写」→ 一个够用的起点:带 date 过滤且预估扫描 < 单节点内存的走 DuckDB,其余走数仓;分流判断本身可以让模型做,因为它已经知道 schema 和分区规则了。附录有完整原始架构图。
docs/architecture.gen.py 渲染成 docs/architecture.svg,
.py 是唯一权威(改结构改 .py 后重渲,不手改 SVG)。
图注里的验证结论:2026-08-31,同一问题四条路径结果完全一致;admin 5.33M 行 vs analyst 2.87M 行(RLS),fee 列被排除。
引擎 A/B 基线见 docs/ab-results.md(2026-08-28 实测)。
设计权威文档为 docs/spec.md(v4,Owner: Xin Jiang, GCR Web3 SA)。
本 deck 的账号标识一律占位化;区域取 us-west-2(引擎)/ us-east-2(公共数据集)。
代码事实截至仓库 main 分支 da591bc:ruff 全清、pytest 40 passed。这页不讲,是留给会后自己看的。它保留了仓库里那张架构图的完整原始形态,包括被正文页裁掉的五步流程图注和底部的验证结论行。
如果有人想要这张图的源文件,直接给 docs/architecture.gen.py —— 它是可执行的规格说明,比 SVG 有用。
_conn_lock(P21)
docs/spec.md —— 设计权威、不变量docs/ab-results.md —— A/B 原始数据docs/architecture.gen.py —— 架构图规格scripts/cleanup.sh —— 一键拆干净
结束页会停留很久(提问都在这一页上),所以中间那栏「带走这四样」是提问的锚点 —— 它把四章各自的一句话结论压成了四行。
收尾一句:这个模式的价值不在 DuckDB 快,在于它让 agent 的试错变得便宜 —— 而 agent 的能力上限,某种程度上就是它能负担多少次试错。