面向 LLM 的程序设计 3:LLM-Friendly 的响应结构:扁平键、稳定字段与类型标注
在满足能力端点与确定性契约之后,响应长什么样仍会直接影响模型能不能「读对结果、少误解、少编造字段」。本系列继续围绕「让 AI 更好理解、更好调用」,讨论如何把 JSON 响应设计成对模型和后续工具链都更友好:键名稳定、层次尽量扁平、数组与对象的语义一眼能读懂,并用明确的标量类型(整数金额的分、布尔库存等)减少歧义。
本篇为系列第三篇:前两篇分别谈了能力端点与 JSON Schema 硬契约;本篇聚焦响应体形态——在契约已成立的前提下,仍要避免「能校验却难理解」的嵌套与命名,让解析与链式调用更省心。
摘要:深层嵌套、缩写键名、同一语义多种字段名、金额与布尔用字符串承载,都会增加模型解析负担与幻觉风险。采用扁平或浅层结构、稳定且可预期的 snake_case 全名、与业务语义一致的类型(如 price_cents: integer、in_stock: boolean),并在顶层保留 response_type 等判别字段,可显著提升 LLM 与程序化下游的消费体验。本文说明原则与正反对照,并给出可运行对比示例。
关键词:LLM-Friendly API;JSON 响应设计;扁平结构;稳定字段;类型标注
源代码链接:面向 LLM 的程序设计 3:LLM-Friendly 的响应结构源代码
1 为什么「有 Schema」还不够?
即便请求与响应都通过了 JSON Schema 校验,仍可能出现:
- 层次过深:结果藏在
data.payload.result.items之下,模型在多步推理中容易数错层级或把兄弟字段张冠李戴。 - 键名不稳定或过于简略:
r、d、list这类缩写对人可读性差,对模型也缺少语义锚点;不同接口分别用id/productId/pid表示同一概念时,跨工具拼接容易混用。 - 标量类型语义模糊:金额用
"19.99"字符串、in_stock用"yes",模型可能输出与下游不一致的格式;整数分 + 货币码、「真假的布尔」更利于确定性处理。 - 数组项形状不一致:有的元素是对象、有的嵌套多一层 wrapper,会给「取第 i 个元素的某字段」这类操作埋下错误种子。
可以把响应想象成给另一位工程师的交接单:Schema 保证「字段合法」,而 LLM-Friendly 结构保证「一眼知道每格写什么、少翻层、少猜缩写」。
2 设计原则小结
| 原则 | 含义 | 💡 理解要点 |
|---|---|---|
| 扁平优先 | 重要结论放在顶层或浅层;避免无信息增量的深套娃 | 像目录页:先见到「有几条、是什么类型」,再展开列表 |
| 键名稳定、可读 | 全词、snake_case、同一概念全局同名(如始终 product_id) |
避免同一文档多种命名风格混用 |
| 类型即语义 | 钱用整数最小单位 + currency;是否库存用 boolean |
减少「字符串里的数字」带来的二次解析 |
| 列表元素同质 | 数组内对象字段集合一致;少用「有时对象有时字符串」 | 便于模型写循环与下游代码复用 |
| 顶层判别 | 可用 response_type(或等价枚举)区分不同成功形态 |
便于分支逻辑与少误读字段 |
🔍 实际例子:搜索商品接口的成功体可设计为 response_type: "product_search"、product_count: 3、products: [{ product_id, title, price_cents, currency, in_stock }],而不是 d: { r: { l: [...] } } 且金额为 "12.50" 字符串。
3 反例与正例(概念对照)
反例(仍可能过校验,但难理解):
- 深层路径:
body.data.results.items[].product.meta.title - 缩写键:
p、amt、flg - 金额:
"19.99"(string),库存:"1"(string)
正例(契约 + 友好):
- 顶层:
response_type、product_count、products - 单项:
product_id、title、price_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,其下为r→l(表示 data / result / list),列表元素为i(商品 id)与m(meta)。 - 标题与价格在
m里缩写为t、amt;其中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_id、title、price_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。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)