用户只让资料助手把报告标题改成“秋季书单”,保存之后,原本存在的副标题却消失了。
模型没有说“删除副标题”。真正出问题的,可能是工具适配器:它为了把参数补齐,把用户没提到的字段写成了 null;接收端恰好把 null 解释为删除。
这是虚构教学场景,不是本站事故。本文只回答一个问题:Agent做局部更新时,怎样分清“没要求改”“明确清空”和“改成新值”?
AI原创概念插画:覆着半透明纸的红花、中央花形空位和右侧金色纸花,借静态并置比喻保留、移除与替换。画面不证明实际编辑动作,也不是接口机制图。
一、一个字段,至少有三种不同的意图
假设报告原来是:
{"title":"夏季书单","subtitle":"给初次阅读者的十个入口"}用户说:“把标题改成秋季书单,其他不动。”如果接口明确采用JSON Merge Patch,最小变更应当是:
{"title":"秋季书单"}没有出现的 subtitle 保持不变。但若适配器生成的是:
{"title":"秋季书单","subtitle":null}含义就不同了。RFC 7396规定,在JSON Merge Patch对象中,null 表示移除对应成员,而不是“没有意见”。[1]
| 发送内容 | 在这个Merge Patch例子里的含义 |
|---|---|
没有 subtitle 键 | 不改副标题 |
"subtitle":null | 移除副标题成员 |
"subtitle":"新读者路线" | 设置新的副标题 |
"subtitle":"" | 设置为空字符串,并非删除成员 |
这里的“至少三种”说的是编辑意图,不是JSON增加了第三种空值。缺失是对象里没有这个键,null 则是键存在、值为空。 JSON Schema官方说明也明确强调,null 与属性不存在并不等价。[2]
更重要的限定是:这张表只描述本例选定的Merge Patch契约。普通JSON的 null 本身不自带删除命令。某个业务接口可能把它解释为“数据库列设为空”、可能拒绝,也可能有另一套规则;不能看见HTTP PATCH 就自动套用RFC 7396。要核实请求格式、媒体类型与服务端文档,尤其是是否使用 application/merge-patch+json。
二、丢失“有没有出现”,往往发生在模型之后
可以把一次Agent编辑拆成四站:
- 用户提出变更;
- 模型生成工具参数;
- SDK、输入模型或适配器转换参数;
- 服务端执行变更。
问题不一定出在第二站。即使模型只输出标题,第三站仍可能把所有可选字段补成默认值。此时,原始参数中的“没给”,在发送前被改写成了“给了空值”。
FastAPI官方教程展示过相邻的风险:更新请求没带旧有税率时,若把带默认值的整个输入模型覆盖存储对象,就可能把原有税率换成默认值。其局部更新示例使用 model_dump(exclude_unset=True),只导出创建输入模型时明确设置的字段,避免用未设置字段的默认值覆盖旧内容。[3]
这是具体框架的示例,不是所有SDK的行为保证。对实际工程来说,应检查的是“字段存在性”是否一路保留下来,而不是只搜索变量里有没有 None。
也不要把“移除所有空值”当通用补救。用户可能确实要求清空;一律过滤 null,会把合法的清空意图也吞掉。类似地,用真假值过滤字段还会误删 false、0 或空数组。保留与清空的区别,应由契约和可信意图决定,而不是由值看起来够不够满决定。
三、一个可以跟随的小实验
下面代码是本地确定性教学实验:输入、报告和字段都是人工构造的,没有运行模型、SDK、HTTP接口或数据库。函数刻意只处理本例的扁平对象,不是完整RFC 7396实现;它只用来观察“补空值”如何改变结果。
import json
original = {"title": "夏季书单", "subtitle": "阅读入口"}
intended = {"title": "秋季书单"}
def apply_flat_patch(target, patch):
result = target.copy()
for key, value in patch.items():
if value is None:
result.pop(key, None)
else:
本轮实际执行得到:
preserve {"subtitle": "阅读入口", "title": "秋季书单"}
filled_null {"title": "秋季书单"}
explicit_delete {"title": "夏季书单"}
empty_string {"subtitle": "", "title": "夏季书单"}第一行和第二行的用户意图相同,区别只在适配器有没有补出 subtitle: null。第二行的副标题已经被移除,不能从“标题改对了”推断整个编辑正确。
第三行说明,显式删除本身并非错误;如果可信任务就是移除副标题,它是预期行为。第四行则说明,空字符串和键不存在依然是两种可观察状态,应用可能需要不同的展示或校验规则。
完整断言还检查了显式 false、0、空数组不会被当成缺失,以及本例输入对象没有被原地改动。断言通过只证明手写规则对这些夹具的行为,不证明模型理解了用户要求,更不证明真实服务采用相同语义。
四、让模型表达动作,而不是猜一个空值
若正在设计新的编辑工具,可以考虑把模型侧意图写得更明确。下面是自定义教学设计,不是标准接口,也没有部署执行:
{
"document_id":"reading-list",
"changes":[
{"field":"title","action":"set","value":"秋季书单"}
]
}没有出现在 changes 里的字段不动;要删除,必须使用明确的删除动作。若业务允许存储真正的空值,还可以分别设计“设为空值”和“移除成员”,不能把两者藏在同一个含糊的 value: null 里。
这种动作表示法的好处,是让“保留”不再依赖模型为每个字段填一个占位符。但代价也真实存在:要维护动作校验、字段允许列表、类型映射与冲突处理。它并不会因为多了 action 就天然安全。
适配器至少应当做到:
- 只允许修改当前工具授权的字段,不接受任意路径写入;
- 检查同一字段是否出现相互冲突的动作;
- 把模型侧动作转换成目标接口真正支持的格式,而不是直接透传;
- 对破坏性变更按业务政策取得确认,并绑定即将执行的具体变更;
- 使用最新对象与版本条件,防止保留旧字段时覆盖并发更新。
已有接口不能随意换契约。如果服务端要求提交完整对象,就先按其文档读取、构造最终对象,并使用版本条件;不能因为本文建议“小补丁”就擅自省略服务端要求的字段。本文讲字段意图,不能替代对象授权、并发控制和发布政策。
五、嵌套对象和数组,不能沿用“看起来差不多”
扁平例子容易理解,实际接口还会有两处陷阱。
第一,JSON Merge Patch会递归处理对象。 比如只修改作者对象中的显示名,不必把作者的所有其他成员重发。但如果适配器先把整个嵌套对象补满空值,误删风险会继续向下传播。不能只在第一层使用“排除未设置”就宣布全链路保真,嵌套输入模型与序列化配置仍要逐层核对。
第二,数组在Merge Patch中不是逐项合并。 RFC 7396指出,不能用它只补丁替换数组中的部分值;数组作为相应值整体替换。[1] 因此,发 "tags": [] 不是“不修改标签”,而是把该数组换成空数组。若要只移除一个标签,应使用接口支持的明确操作,或读取最新数组后按版本条件提交完整目标数组。
另一个边界是:Merge Patch用 null 表示成员移除,因此不适合直接表达“把这个成员的值设为真正的JSON null”。如果产品需要区分“有一个空值成员”与“成员完全不存在”,就应选择能清楚表达这一区别的契约,而不是强行套用格式。
这些是格式本身的取舍,不是模型多读几遍提示词就能消除的问题。
六、给Agent编辑工具的一份短验收单
发布一个局部编辑工具前,可以按以下顺序检查:
- 每个可编辑字段的缺失、
null、空字符串、false、0、空数组分别是什么意思?哪些值应拒绝? - 原始工具参数有没有保留“哪些键实际出现”的信息?SDK和适配器有没有补值?
- 缺省字段是否确实不改旧内容?显式清空是否又被错误过滤了?
- 嵌套对象与数组的处理是否符合当前接口,而不是套用扁平字典直觉?
- 从同一旧对象分别运行保留、设置、清空夹具,检查完整终态而不只检查目标字段。
- 有一份“只改标题”的负向验收,确认副标题、标签等未授权字段未变。
- 变更预览是否展示真实发送载荷的效果,而不是模型早先生成的计划?
- 最新版本条件、字段权限、必要确认和写后回读是否独立成立?
最值得保留下来的信息,有时不是某个具体值,而是这个字段根本没有被要求修改。如果运行层在转换参数时把这条信息抹掉,再严整的JSON也可能把一次小修改变成一场安静的清空。
来源、日期与阅读范围
本文由AI创作。报告、书单、动作格式均为虚构教学材料;实际执行了上文Python精简代码与额外断言,未运行Agent、LLM、FastAPI、Pydantic、数据库或远端更新服务。工程建议不是本站API说明。
资料检索截止:2026年10月24日16:57 UTC。
- RFC 7396:JSON Merge Patch,2014年10月。直接获取公开原始文本,实际阅读第1、2节及第3节开头,使用其成员移除、对象递归、数组整体替换与显式null限制;不声称测试了完整协议实现。
- JSON Schema:null,动态说明页,未核实独立发布日期或固定版本。实际阅读短页中null与属性缺失的区分,不把类型约束当成业务删除语义。
- FastAPI:Body — Updates及官方仓库对应Markdown,动态教程,master分支未锁定发布版本。实际阅读覆盖警告、局部更新与
exclude_unset章节;网页直接补读403,改读官方仓库相关正文。这里只引用教程展示的转换原则,未实测框架行为。


留下你的想法 0
欢迎提问、补充和不同观点。评论通过审核后会公开显示。