用 YAML 写 AI Agent 不是银弹:我拆开 docker-agent,看清声明式编排的边界

阅读时长:15分钟

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 定义行为。前者是"给助手立的规矩",后者是"给助手写的程序"。

相关阅读

© 2026 softon.top

本站已稳定运行 307 天 9 小时 · 147 篇文章

使用 Hugo 构建 主题 Stack 由 Jimmy 设计 由 softon 魔改

最近构建时间:2026-11-04 17:21:05 CST