面向对象配置:为什么TOML是智能体系统唯一的选择

配置语言塑造了开发人员如何思考他们的系统。我们选择TOML是因为它将面向对象的思想引入到配置概念中,而不是嵌套的元数据。

配置语言有争议
开发人员对配置格式有很强的意见。YAML的拥护者欣赏干净的外观和最小的语法。JSON支持者喜欢显式结构和通用工具。INI用户重视简单性。每种选择都涉及到权衡,当您配置的东西既需要人类可读,又需要机器可靠时,这些权衡就很重要。

我们都被配置烧伤了。YAML将no转换为false。JSON去掉尾随逗号。INI没有嵌套。这是一种认知开销,会分散您对实际配置内容的注意力。

如果配置可以像代码一样思考呢?

TOML保持配置显式
当我们需要一个具有复杂验证规则和嵌套测试用例的系统的配置格式时,我们评估了通常的可疑情况。YAML将给我们干净的语法,但不可预测的解析。JSON将是显式的,但冗长且不可原谅。我们需要开发人员可以读、写和信任的东西,而不需要与格式作斗争。

TOML之所以突出,是因为它是明确声明的。你写的就是你得到的。没有类型强制转换意外,没有不可见的空白依赖,没有结构上的模糊性。但与JSON的冗长不同,TOML具有使配置真正令人愉快地编写的语法特性。

无显式嵌套的点表示法
这就是TOML变得有趣的地方。您可以使用点表示法引用嵌套属性,而无需先显式定义父结构:

[eval]
description = "Test response accuracy"
type =
"accuracy"
targets.agents = [
"*"]
targets.tools = []

注意到targets.agents和targets.tools了吗?我们不需要写:

[eval]
description = "..."
type =
"accuracy"

[eval.targets]
agents = [
"*"]
tools = []

我们只是在内联中使用了点表示法。结构是隐含的,但不含糊。TOML自动创建targets对象。这是一个小东西,加起来,当你写几十个广告。它允许您根据属性而不是嵌套深度进行思考。然而,如果您喜欢这种风格,您可以自由地构建完整的嵌套结构;在我们具有这种灵活性的情况下,我们优先将类似的概念分组在一起以保持清晰。

表数组:可重复结构
配置通常涉及使用不同的值重复相同的结构测试用例、服务器定义、部署目标。TOML的[[double bracket]]语法可以干净地创建表数组:

[[eval.cases]]
prompt = "What is the capital of France?"
output.contains =
"Paris"

[[eval.cases]]
prompt =
"Explain what HTTP is"
output = {
  similar =
"HTTP is a protocol for transferring data over the web.",
  threshold = 0.8
}

[[eval.cases]]
prompt =
"Get weather data"
[eval.cases.output.schema]
temperature = { type =
"float" }
condition = { type =
"str" }
humidity = { type =
"int" }

每个[[eval.cases]]声明都向数组添加另一个项。结构在视觉上是独特的。重复是显而易见的。您可以扫描条目而不会在嵌套中迷失方向。

作为行为修饰符的点表示法
TOML的点语法在这里实现了一些巧妙的功能:您可以使用它将行为修饰符附加到字段,而无需引入额外的嵌套。不是将配置包装在容器对象中,而是扩展字段名称本身:

[[eval.cases]]
prompt = "What is the capital of France?"
output.contains =
"Paris"

[[eval.cases]]
prompt =
"What's the weather like?"
output.startswith = [
"It's", "Currently", "The weather"]

[[eval.cases]]
prompt =
"Extract the user's email address"
output.match =
"\w+@\w+\.\w+"

每个后缀-.contains,.startswith,.match-改变值的解释方式。字段名称具有语义含义。您不是在嵌套对象或添加元数据层。你用钥匙本身来表达意图。

这是应用于配置的面向对象思想。你有一个概念。你可以对它进行操作,这是开发人员已经在思考的问题,现在它是配置的工作方式。当有人看到output.contains时,他们会立即理解该模式。他们可以凭直觉知道还有哪些操作存在。与YAML或JSON相比,你需要包装器对象:

{
  "output": {
   
"strategy": "contains",
   
"value": "Paris"
  }
}

或嵌套结构:

output:
  contains: "Paris"

TOML保持平坦。字段名描述了行为,值只是值-没有额外的层包装。

此模式扩展到模式定义。字段有效性规则直接附加到字段名称:

[[eval.cases]]
prompt = "Validate user profile"
[eval.cases.output.schema]
username = {
  type =
"str",
  min_length = 3,
  max_length = 20
}
age = {
  type =
"int",
  min = 13,
  max = 120
}

验证约束是可读的和自文档化的。没有包装器对象,没有间接性只有直接的字段定义及其验证规则。

用于紧凑结构的内联表
当配置对象很小时,TOML允许您内联编写它们:

latency = { max_ms = 3000 }
output = { similar = "expected text", threshold = 0.8 }
tools = [{ name =
"add", args = [2, 2] }]

这比将每个小对象分割成自己的[section]块更可读,但它仍然是显式的。您可以确切地看到哪些是嵌套的,哪些不是。

TOML做对了什么
生态系统采用:Python的打包生态系统基于TOML标准化。pyproject.toml取代了setup.py,成为Python项目的官方配置格式。Poetry、Ruff、Black、pytest都使用TOML。当以实用主义和谨慎的设计决策而闻名的Python社区选择TOML作为事实上的标准时,这是一个值得关注的信号。

显式类型:字符串需要引号。数字不会。布尔值是true或false,而不是yes/no/on/off。日期是ISO 8601。不用猜。

可读结构:键不加引号,除非它们需要特殊字符。部分使用视觉上突出的[headers]。注释和大多数脚本语言一样使用#。

没有不可见的空白:与YAML不同,缩进是装饰性的。你的配置不会因为某人的编辑器使用制表符而不是空格而中断。我们选择了TOML,知道这将消除整个类别的错误。

我们接受的权衡
TOML并不完美。深度嵌套的结构会变得冗长--你最终会得到很长的虚线路径或多个[section.subsection.subsubsection]头。如果你的配置是一个深度嵌套的树,JSON或YAML可能会更自然。

TOML也不像YAML那样支持引用或锚。你不能定义一个块,然后在其他地方重用它。对于可重用性比显式性更重要的配置,这是一个真实的限制。

TOML比JSON更不受普遍支持。大多数语言都有库,但JSON无处不在。如果您需要在不起眼的环境或遗留系统中解析配置,那么JSON的普遍性将获胜。

TOML会更好吗?
我们注意到一个模式:当你在一个表数组的深处定义属性时,你必须重复完整的路径:

[[eval.cases]]
prompt = "Test prompt"
  [eval.cases.output.schema]
  field1 = { type =
"str" }
  field2 = { type =
"int" }

[[eval.cases]]
prompt =
"Another test"
  [eval.cases.output.schema]
  field1 = { type =
"str" }
  field2 = { type =
"int" }

[eval.cases.output.schema]是一个大问题。如果TOML支持一种简写,其中前导点表示“相对于父表”,那会怎么样?

[[eval.cases]]
prompt = "Test prompt"
  [.output.schema]
  field1 = { type =
"str" }
  field2 = { type =
"int" }

这将指示“相对于最后一个表数组项”。“这是一个小的语法特性,可以减少数组项下嵌套较深的对象中的视觉噪音。也许这值得作为TOML扩展进行研究,或者它引入了模糊性,破坏了使TOML工作的明确清晰性。值得考虑。

为什么配置格式很重要
配置不断被读取和写入。开发人员在定义行为时编写它。他们在调试的时候读它。他们在代码审查期间扫描它。当需求改变时,他们会修改它。格式需要立即清晰且可预测。

当配置的系统已经很复杂或不确定时,这一点尤其重要。配置不应该增加另一层的认知开销这是我们开始时的问题。TOML的声明清晰性使开发人员可以专注于他们正在配置的内容,而不是格式将如何解释它。

对于Agent CI,TOML是我们的主要集成点。评估配置存在于.agentci/evals/中,并定义所有测试行为,而无需更改应用程序代码。这种分离意味着您可以向现有代理添加全面的评估范围,而无需修改提示、工具或部署逻辑。配置格式成为接口。它需要可读性强、可维护性好,并且有足够的表达能力来处理复杂的验证规则。TOML给了我们这些。

更广泛的模式
这个选择反映了一个更大的哲学:开发人员工具应该是可预测的。惊喜属于你正在构建的东西,而不是你用来构建它的工具。

YAML的类型强制转换可能会节省一些字符,但当Norway变成true时会产生混淆,因为它匹配布尔别名。JSON的严格结构消除了歧义,但让您计算括号并记住逗号。TOML找到了中间立场--足够明确,可以预测,足够表达,令人愉快。

配置语言塑造了开发人员如何思考他们的系统。一个好的配置格式使结构清晰,减少仪式,并且不会让你感到惊讶。TOML就是这么做的。

如果您想知道这些模式在实践中是如何发挥作用的,那么我们作为示例使用的评估工具是开源的:github.com/Agent-CI/client-config。如果这与您遇到的配置挫折产生共鸣-或者如果您发现了其他有效的模式-我们想听听。配置设计是迭代的。我们能学到的真实世界的用法越多越好。