在满足能力端点与确定性契约之后,响应长什么样仍会直接影响模型能不能「读对结果、少误解、少编造字段」。本系列继续围绕「让 AI 更好理解、更好调用」,讨论如何把 JSON 响应设计成对模型和后续工具链都更友好:键名稳定、层次尽量扁平、数组与对象的语义一眼能读懂,并用明确的标量类型(整数金额的分、布尔库存等)减少歧义。

本篇为系列第三篇:前两篇分别谈了能力端点JSON Schema 硬契约;本篇聚焦响应体形态——在契约已成立的前提下,仍要避免「能校验却难理解」的嵌套与命名,让解析与链式调用更省心。

摘要:深层嵌套、缩写键名、同一语义多种字段名、金额与布尔用字符串承载,都会增加模型解析负担与幻觉风险。采用扁平或浅层结构、稳定且可预期的 snake_case 全名与业务语义一致的类型(如 price_cents: integerin_stock: boolean),并在顶层保留 response_type判别字段,可显著提升 LLM 与程序化下游的消费体验。本文说明原则与正反对照,并给出可运行对比示例。

关键词:LLM-Friendly API;JSON 响应设计;扁平结构;稳定字段;类型标注

源代码链接:面向 LLM 的程序设计 3:LLM-Friendly 的响应结构源代码


1 为什么「有 Schema」还不够?

即便请求与响应都通过了 JSON Schema 校验,仍可能出现:

  1. 层次过深:结果藏在 data.payload.result.items 之下,模型在多步推理中容易数错层级或把兄弟字段张冠李戴。
  2. 键名不稳定或过于简略rdlist 这类缩写对人可读性差,对模型也缺少语义锚点;不同接口分别用 id / productId / pid 表示同一概念时,跨工具拼接容易混用。
  3. 标量类型语义模糊:金额用 "19.99" 字符串、in_stock"yes",模型可能输出与下游不一致的格式;整数分 + 货币码、「真假的布尔」更利于确定性处理。
  4. 数组项形状不一致:有的元素是对象、有的嵌套多一层 wrapper,会给「取第 i 个元素的某字段」这类操作埋下错误种子。

可以把响应想象成给另一位工程师的交接单:Schema 保证「字段合法」,而 LLM-Friendly 结构保证「一眼知道每格写什么、少翻层、少猜缩写」。


2 设计原则小结

原则 含义 💡 理解要点
扁平优先 重要结论放在顶层或浅层;避免无信息增量的深套娃 像目录页:先见到「有几条、是什么类型」,再展开列表
键名稳定、可读 全词、snake_case、同一概念全局同名(如始终 product_id 避免同一文档多种命名风格混用
类型即语义 钱用整数最小单位 + currency;是否库存用 boolean 减少「字符串里的数字」带来的二次解析
列表元素同质 数组内对象字段集合一致;少用「有时对象有时字符串」 便于模型写循环与下游代码复用
顶层判别 可用 response_type(或等价枚举)区分不同成功形态 便于分支逻辑与少误读字段

🔍 实际例子:搜索商品接口的成功体可设计为 response_type: "product_search"product_count: 3products: [{ product_id, title, price_cents, currency, in_stock }],而不是 d: { r: { l: [...] } } 且金额为 "12.50" 字符串。


3 反例与正例(概念对照)

反例(仍可能过校验,但难理解)

  • 深层路径:body.data.results.items[].product.meta.title
  • 缩写键:pamtflg
  • 金额:"19.99"(string),库存:"1"(string)

正例(契约 + 友好)

  • 顶层:response_typeproduct_countproducts
  • 单项:product_idtitleprice_cents(int)、currency(如 CNY)、in_stock(bool)
  • 可选:统一分页 next_cursor(string | null)与 Schema 对齐,避免魔法嵌套

完整字段定义见配套 demo 中的 Pydantic 模型与 OpenAPI。


4 本示例在做什么?

配套 demo同一套内存里的 mock 商品(例如 sku_1001 无线鼠标、sku_1002 机械键盘,含原价整数分、币种、是否有货),经同一个过滤条件生成两种响应;两端都走 FastAPI + Pydantic,OpenAPI /docs 里能看到两套不同的响应 Schema,但都满足校验。

共同请求POST,请求体 JSON 为 ProductSearchRequest,字段只有 query(字符串,可空)。服务端对 MOCK_PRODUCTS标题子串匹配(不区分大小写):query 为空则返回全部商品;query 非空则只保留 title 中含该子串的项。main.py 里固定发送 {"query": "键盘"},因此只会命中「机械键盘」一条,便于两份响应在「条数相同」的前提下纯粹对比形状

1)POST /examples/nested-legacy(反例)

  • 响应根上是缩写键 d,其下为 rl(表示 data / result / list),列表元素为 i(商品 id)与 m(meta)。
  • 标题与价格在 m 里缩写为 tamt;其中 amt 是字符串,由内部 price_cents 格式化成类似 "99.00" 的小数字符串,故意丢掉「整数分 + 币种」的确定性表达。
  • 若要用自然语言描述取一条商品的标题:需要走路径大致如 d.r.l[0].m.t,id 为 d.r.l[0].i不建议在生产照搬这一形状。

2)POST /examples/flat-llm-friendly(正例)

  • 顶层依次给出 response_type(本 demo 固定为 "product_search",用于多态判别)、product_count(整数条数)、products(数组)。
  • 数组元素是同质对象:product_idtitleprice_cents整数最小货币单位)、currency(三字母,如 CNY)、in_stock布尔)。与同一条 mock 数据源对齐,但不再把价格压成无币种的字符串。

如何对照:先 uvicorn server_api:app --reload --port 8312,再在项目目录运行 python main.py;终端会先后打印两次美化后的 JSON。读者可以数一下:反例从根到「第一条商品标题」经过几层键名、正例是否一眼能读到 products[0].title;并打开 http://127.0.0.1:8312/docs 展开两个 POST,对照自动生成的响应 Schema 与字段说明。

请添加图片描述

启动服务后,再在另外一个terminal中运行 python main.py,terminal中返回:

=== 1. 反模式:POST /examples/nested-legacy ===

请求: {'query': '键盘'}
响应 JSON(美化):
 {
  "d": {
    "r": {
      "l": [
        {
          "i": "sku_1002",
          "m": {
            "t": "机械键盘",
            "amt": "459.00"
          }
        }
      ]
    }
  }
}

=== 2. LLM-Friendly:POST /examples/flat-llm-friendly ===

请求: {'query': '键盘'}
响应 JSON(美化):
 {
  "response_type": "product_search",
  "product_count": 1,
  "products": [
    {
      "product_id": "sku_1002",
      "title": "机械键盘",
      "price_cents": 45900,
      "currency": "CNY",
      "in_stock": false
    }
  ]
}

5 完整代码与文档

  • 运行与架构README_运行与架构.md
  • 完整方案README_完整方案.md

在项目目录下执行 pip install -r requirements.txt,启动 uvicorn server_api:app --reload --port 8312 后,另开终端运行 python main.py


Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐