2026 年 10 月底,Hacker News 上出现一篇《Writing AI agents in YAML instead of Python》,指向 codecut.ai 的一篇教程。
内容说的是 Docker 的新工具 docker-agent:不写 Python,一个 YAML 文件定义整个 Agent,一条命令跑起来。
我读完文档和教程,第一反应是——这跟 2023 年"低代码 Agent"那波口号有什么区别? 但仔细拆下去,发现它确实解决了一个真问题:Agent 代码膨胀后,改一处要读三处。
但它的边界也很清楚。这篇把机制拆开,讲清楚什么时候该用 YAML,什么时候必须写代码。
TL;DR
- docker-agent 是 Docker 官方开源工具,一个 YAML 定义 Agent 的模型、指令、工具、拓扑
- 它解决的是"Agent 代码膨胀"问题:配置与代码分离,一处改动不用翻三个文件
- 微软 Agent Framework 1.0 也在走同一条路:YAML 声明式 Agent + 编排拓扑
- 边界:自定义分支、重试、逐步控制——YAML 表达不了,必须回 LangGraph
- 实测数据:gpt-5-mini 通过 18/18 断言,gpt-5-nano 只过 13/18,成本却只要一半
Agent 代码为什么会膨胀?
先看传统写法要干什么。Google DeepMind 的 Philipp Schmid 在 2026 年 9 月那场《Agents Without Code》演讲里,把流程列得很直白:
写一个 Agent,主要就是写一个 Python 循环。你需要定义一个 JSON schema,定义 Python 函数,看 LLM 的输出,检查它是函数调用还是文本回复,然后跟类型匹配,再调用工具,出错了还要回头重来。
这段话的关键在最后那句"出错了还要回头重来"。真实 Agent 的代码长这样:
# 一个最小 tool-calling agent 的骨架(示意)
def run_agent(messages, tools):
while True:
resp = client.chat.completions.create(
model="gpt-4", messages=messages, tools=tools
)
msg = resp.choices[0].message
if msg.tool_calls:
for call in msg.tool_calls:
result = dispatch(call.name, call.arguments) # ← 错误处理在这
messages.append(tool_result(result))
else:
return msg.content # ← 循环出口在这
# 重试、超时、token 上限、状态持久化……全要自己写
问题在于:模型、工具、指令、循环逻辑、错误处理全糊在一个文件里。
改一个提示词要动代码,加工具要动代码,换模型要动代码。Agent 一复杂,就成了没人敢碰的祖传代码。
docker-agent:把 Agent 压进一个 YAML
docker-agent 的做法是——配置归配置,逻辑归运行时。
一个 embeddings.yaml 定义全部:
agents:
root:
model: openai/gpt-5-mini
description: Answers questions about Polars from the user guide
instruction: |
You answer questions about the Polars DataFrame library.
Always search the knowledge base before answering.
Answer only from what the search returns.
toolsets:
- type: rag
ref: polars_docs
rag:
polars_docs:
tool:
description: The Polars user guide
docs: [./docs/polars]
strategies:
- type: chunked-embeddings
embedding_model: openai/text-embedding-3-small
vector_dimensions: 1536
跑起来就一条命令:
docker agent run --exec embeddings.yaml "How do I read a Parquet file without loading it all into memory?"
关键点:循环、错误处理、工具调用、类型匹配——这些运行时全管了。你只描述"要什么",不写"怎么做"。
30 个内置工具集
docker-agent 内置 30 个 toolset,教程里点名的有:
| 工具集 | 作用 |
|---|---|
rag |
检索增强,向量存本地 SQLite,自动索引、文件变更自动重索引 |
shell |
跑 shell 命令 |
duckdb |
查 DuckDB |
filesystem |
读写文件 |
fetch |
抓网页 |
mcp |
接 MCP 工具 |
memory |
跨会话记忆 |
rag 那个细节值得说:传统 RAG 要自己搭向量数据库、自己写索引脚本。docker-agent 把向量存本地 SQLite,首次启动自动索引,文件变了自动重索引。这一步省掉的是整个基础设施。
flavor:一个文件的多个变体
YAML 最容易遇到的问题是"想试个变体就得复制一份"。docker-agent 的 flavors 段解决这个:
# embeddings.yaml
agents:
root: ...
rag:
polars_docs:
...
strategies:
- type: chunked-embeddings
flavors: # 文件末尾新增
hybrid:
rag:
polars_docs:
strategies+: # + 号表示"在原有基础上追加"
- type: bm25
results:
fusion:
strategy: rrf
k: 60
deduplicate: true
limit: 5
strategies+ 那个 + 号是合并语义——保留原来的 embedding 搜索,追加 bm25 关键词搜索,再用 RRF(Reciprocal Rank Fusion)融合结果。
教程里有个真实对比:问 SQLContext(一个类名),纯 embedding 搜索找不到(返回"没找到匹配文档"),加 hybrid flavor 后关键词搜索命中,正确回答。
eval:把 Agent 当软件测试
这是我印象最深的部分。evals/ 目录下放 JSON 测试用例:
{
"title": "Large Parquet file question",
"messages": [
{"message": {"role": "user", "content": "How do I read a Parquet file without loading it all into memory?"}}
],
"evals": {
"assertions": [
{"name": "uses scan_parquet", "type": "contains", "value": "scan_parquet"},
{"name": "mentions streaming", "type": "contains", "value": "streaming"},
{"name": "searched the docs", "type": "tool_called", "value": "polars_docs"}
]
}
}
三种断言:contains(答案含某词)、tool_called(确实调了某工具)、还有别的类型。跑测试:
docker agent eval embeddings.yaml ./evals
实测输出(教程数据):
| 指标 | gpt-5-mini | gpt-5-nano(cheap flavor) |
|---|---|---|
| 断言通过(各跑 3 次) | 18/18 | 13/18 |
| Parquet 用例 | 3/3 runs | 0/3 runs |
| asof join 用例 | 3/3 runs | 2/3 runs |
| 总成本 | $0.015 | $0.007 |
便宜模型省一半成本,但断言从 18 掉到 13,Parquet 用例三次全挂。这种对比靠肉眼看输出是看不出来的——你必须有断言。
而且每个 eval 跑在独立容器里,一个测试不会留下文件影响另一个。
share:像分享镜像一样分享 Agent
docker agent share push embeddings.yaml docker.io/<username>/polars-docs:v1
# 同事
docker agent run docker.io/<username>/polars-docs:v1
不用装 Python 环境、不用跑 setup 脚本。注意:只共享 YAML 文件,它指向的 docs 目录、prompt 文件必须在你运行 Agent 的目录里存在。
微软也在这条路上:Agent Framework 1.0
docker-agent 不是孤例。微软在 2026 年 4 月 3 日 GA 了 Agent Framework 1.0(Python + .NET 同步,MIT 协议),把 Semantic Kernel 和 AutoGen 合并。
它的核心特性之一,官方原话:
Declarative Agents and Workflows (YAML) – Define agents’ instructions, tools, memory configuration, and orchestration topology in version-controlled YAML files, then load and run them with a single API call.
翻译:在版本控制的 YAML 里定义 Agent 的指令、工具、记忆配置、编排拓扑,然后一次 API 调用加载运行。
注意"orchestration topology"(编排拓扑)这个词——微软把多 Agent 的拓扑结构也放进 YAML 了,不只是单个 Agent 的配置。
同时它还提供迁移助手:分析你现有的 Semantic Kernel / AutoGen 代码,生成逐步迁移计划。这是给已经在写代码的团队准备的退路。
声明式 vs 命令式:边界在哪?
现在可以回答最初的问题了。两种范式的真正区别:
| 维度 | 声明式(YAML) | 命令式(Python/代码) |
|---|---|---|
| 表达什么 | 要什么(目标、工具、拓扑) | 怎么做(每一步逻辑) |
| 控制粒度 | 粗(框架决定循环) | 细(你写每一行) |
| 自定义分支 | 难(YAML 没有 if/else 语义) | 容易 |
| 重试/回退 | 依赖框架内置策略 | 完全自定义 |
| 状态持久化 | 框架管 | 自己设计 |
| 改配置 | 改 YAML,不碰代码 | 改代码 |
| 可测试性 | 靠 eval 断言 | 靠单元测试 + eval |
| 上手门槛 | 低 | 高 |
| 适合 | 固定流程、工具组合、RAG 问答 | 复杂分支、长程任务、精细控制 |
教程作者自己的结论,我觉得很诚实:
docker-agent is not the right fit for everything. If you need to control each step the agent takes, with custom branches or retries, a code-first framework like LangGraph is a better fit.
“不适合所有场景。如果你需要控制 Agent 的每一步、自定义分支或重试,LangGraph 这种代码优先的框架更合适。”
LangGraph 的对照:它把编排暴露成有向图——节点是 Python 函数,边定义控制流,状态显式声明,框架管持久化和状态转移。前期工作量更大,但对需要分支、重试、人类审批关卡的流程,能力强得多。它还支持 time-travel debugging(从任意 checkpoint 重放)。
我的判断:按任务形态选,不是按潮流选
拆完之后,我的结论是没有银弹,只有匹配。
适合 YAML 的场景
- RAG 问答:文档检索 + 回答,流程固定,工具就是 rag + fetch
- 内容流水线:采集 → 清洗 → 生成,线性流程
- 配置驱动的多 Agent:同一套工具,不同模型/指令组合(flavor 就是干这个的)
- 非 Python 用户维护:运营、产品要改指令,不用碰代码
- 需要 eval 回归:改完跑断言,比看输出靠谱
必须写代码的场景
- 复杂分支:根据中间结果走不同路径
- 长程任务:几十步、需要 checkpoint 和恢复
- 精细重试:特定错误重试、退避策略、降级
- 人类审批关卡:某步必须人工确认才继续
- 需要 time-travel 调试:从任意历史状态重放
一个折中方案
微软那条路其实给了提示:YAML 定义拓扑 + 代码实现节点。
- 用 YAML 声明"有几个 Agent、各自什么模型、什么工具、怎么连"
- 用 Python 写每个节点内部的具体逻辑
- 框架管编排、持久化、迁移
这样既拿到 YAML 的可读性和版本控制,又不丢代码的灵活性。docker-agent 的 mcp 工具集和微软的"单 API 调用加载 YAML",本质上都在往这个方向走。
成本账本
| 项目 | docker-agent | 手写 Python Agent | LangGraph |
|---|---|---|---|
| 初始代码量 | 一个 YAML(~30 行) | 一个 .py(~200 行起) | 一个图定义(~100 行) |
| 依赖 | Docker Desktop 4.63+ | Python + 各 SDK | Python/TS + LangGraph |
| RAG 基础设施 | 内置(SQLite 向量) | 自己搭(向量库 + 索引) | 自己搭 |
| eval 框架 | 内置断言 | 自己写 | 自己写(或用 LangSmith) |
| 迁移成本 | 低(改 YAML) | 高(改代码) | 中 |
| 学习曲线 | 平缓 | 陡 | 中陡 |
按教程数据算:一个 eval 用例成本 $0.002 量级。如果你的 Agent 每天跑 1000 次,月成本在几十美元量级——这个量级下,代码可维护性比那点性能差异重要得多。
常见问题
Q1: docker-agent 是免费的吗?
A: 开源工具,MIT 协议。装在 Docker Desktop 4.63+ 里(docker agent 命令自带),或 brew install docker-agent。跑 Agent 要调 LLM API,那部分按 token 付费。
Q2: YAML 写 Agent 会不会丢掉灵活性?
A: 会,但只在"需要逐步控制"的场景。如果你要自定义分支、重试、人类审批关卡,YAML 表达不了。教程作者自己也说这种情况该用 LangGraph。
Q3: docker-agent 和 LangGraph 能一起用吗?
A: 可以。docker-agent 有 mcp 工具集,能接外部工具;LangGraph 能调 MCP。实践中常见做法是:固定流程用 YAML 声明,复杂节点用 Python 写,通过 MCP 拼起来。
Q4: 为什么微软也要做 YAML 声明式?
A: 因为代码膨胀是真痛点。Agent 一复杂,模型/工具/指令/循环糊在一个文件里,改一处要读三处。YAML 把配置抽出来,版本控制能看到每次改了什么。微软的迁移助手也说明:他们知道很多团队已经在写代码了,给的是退路不是推倒重来。
Q5: eval 断言能替代人工检查吗?
A: 不能完全替代。断言能抓住"答案是否含关键词"“是否调了工具"这类回归,但语义正确性、语气、格式还是要人看。它的价值是防止你改配置时悄悄改坏——教程里 gpt-5-nano 通过率从 18 掉到 13,靠肉眼根本发现不了。
Q6: 我应该从哪开始?
A: 如果你现在没有 Agent,先用 docker-agent 或微软 Agent Framework 的 YAML 跑一个 RAG 问答,感受"配置即 Agent”。等遇到 YAML 表达不了的需求,再上 LangGraph。反过来先学 LangGraph 再降维,学习成本高得多。
Q7: AGENTS.md 和这个是一回事吗?
A: 不是。AGENTS.md 是给编码 Agent(Claude Code、Cursor)读的项目规则文件,告诉它们代码库约定。docker-agent 的 YAML 是给你自己搭的 Agent 定义行为。前者是"给助手立的规矩",后者是"给助手写的程序"。