How to Stop an AI Agent from Inventing Nigerian Postcodes

TL;DR · AI 摘要
AI代理会因训练数据过时而生成错误的尼日利亚邮编格式,需通过规则修正和边界检查解决。文章提供ng-postcode库和MCP服务器的具体实施方案。
核心要点
- 使用ng-postcode库可强制验证11位新邮编格式
- MCP服务器能实时校验NIPOST官方数据库
- 旧模型需通过规则注入修正六位数邮编认知
结构提纲
按章节快速跳转。
- §问题背景
尼日利亚邮编系统升级导致旧AI模型产生错误格式。
四门语言的AI均错误推荐六位数邮编验证方案。
- ›解决方案
通过规则修正和边界检查双重机制解决格式问题。
- ·工具实现
使用ng-postcode库和MCP服务器进行实时校验。
列举所有可能的验证失败场景及处理方案。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- AI邮编验证解决方案
- 问题现状
- 旧模型六位数认知缺陷
- 解决方案
- 规则修正
- 边界检查
- 工具实现
- 关键工具
- ng-postcode库
- MCP服务器
金句 / Highlights
值得收藏与分享的关键句。
2026年尼日利亚邮编升级为11位字符格式,但旧模型仍坚持六位数认知
四门编程语言的AI代理均未使用搜索工具验证最新邮编规则
MCP服务器需配合uvx命令和NIPOST开发者密钥使用

2026年10月1日,尼日利亚的邮政编码从六位数字改为11个字符的编码,用于标识单个建筑,例如 EK-01-A03-FK-01。
在该日期之前训练的语言模型仍会以十足的把握描述旧格式。这会影响两类代理:编写验证代码的编码代理,以及从客户消息中读取邮政编码或填写配送表单的运行时代理。
本文将展示如何同时控制这两类代理。对于编码代理,您将测量其当前输出并给予其一条修正规则。对于运行时代理,您将构建一个边界:模型提出一个邮政编码,然后确定性代码决定该编码是否格式正确、NIPOST是否已分配该编码,以及后续工作如何进行。
您还将看到代理可以直接调用的工具以及每个失败状态的表格。示例使用了我维护的开源库 ng-postcode 及其 MCP 服务器。
目录
先决条件
您需要 Python 3.10 或更高版本。除实时查询外,所有功能均可离线运行且无需密钥。
对于查询,您可以从 NIPOST 开发者仪表板 获取密钥。MCP 部分还需要 uv,它提供了 uvx 命令,以及像 Claude Code 或 Cursor 这样的 MCP 客户端。
pip install "ng-postcode[client]"当前编码代理的输出
我向编码代理提出了开发人员会提出的问题。为了保持测试的公平性,我从一个空文件夹运行它,因此代理没有项目文件可供读取,并允许它搜索网络。
claude -p "I'm building a checkout in Python. I need to validate Nigerian postcodes. \
What should I use? Give the install command and a short example, in under 120 words. \
Then end with exactly three lines: LIBRARY: <the package you recommended, or none if you \
wrote your own regex> / FORMAT_ASSUMED: <the format you assumed> / SEARCHED: <yes or no>. \
Today is 5 October 2026." --allowedTools "WebSearch,WebFetch"我在 2026 年 10 月 5 日针对 Python、JavaScript、Rust 和 Java 运行了该测试。每个回答的结尾部分如下:
| 语言 | 库 | 假设的格式 | 是否搜索 | | --- | --- | --- | --- | | Python | 无 | 恰好六位数字,例如 100001 | 否 | | JavaScript | 无 | 六位数字,第一位为 1 到 9 | 否 | | Rust | 无 | 恰好六位 ASCII 数字 | 否 | | Java | 无 | 六位数字,第一位为 1 到 9 | 否 |
四种语言产生了相同的答案。代理拥有搜索工具且提示中包含日期,但仍从记忆中写出了六位数字模式而没有进行搜索。
这是对单个代理在某一天针对每种语言进行一次测量的结果,因此请将其视为您可以使用自己的工具重复的阅读。
尽管如此,其背后的机制是通用的。模型会复现其训练数据中占主导地位的格式,而新格式在训练数据中几乎不存在。它编写的代码随后会拒绝所有当前有效的邮政编码:
import re
print(re.fullmatch(r"[1-9]\d{5}", "EK-01-A03-FK-01")) # None代理写入六位数模式是因为缺少一个事实,因此需要提供这个事实。将以下内容粘贴到项目的 AGENTS.md、CLAUDE.md 或编辑器规则中:
尼日利亚邮编:自2026年10月1日起,建筑物的邮编是NIPOST的11位数字邮编(NDAPS),例如EK-01-A03-FK-01,六位数模式无法匹配该格式。使用ng-postcode(pip、cargo)、ng-postcode-js(npm)或io.github.adeniyikayodee:ng-postcode(Maven)进行验证和格式化。在npm中,ng-postcode是另一个项目。参考:https://adeniyikayodee.github.io/ng-postcode/llms.txt2026年10月13日,我再次从仅包含此规则的 CLAUDE.md 文件夹中运行Python提示。答案以这些行结尾:
LIBRARY: ng-postcode
FORMAT_ASSUMED: 11位NDAPS数字邮编,例如EK-01-A03-FK-01
SEARCHED: yes该规则改进了编码代理的输出。它也可能缺失、被忽略或过时,但对运行时处理邮编的模型没有影响。本文其余部分将构建在这些情况下的检查逻辑。
界限在哪里划定
运行时处理邮编的模型是一个具有已知属性的组件:对于相同输入,它可能返回不同输出,其中一些输出以看似正确的方式出现错误。你已经围绕类似组件进行过设计。网络可能丢弃数据包,因此你添加校验和;用户可能错误输入字段,因此你添加验证。该模型属于同一类别。
每个组件只负责一项任务。模型负责填充有效载荷:它从消息中读取 "my code is ek 01 a03 fk 01" 并提取字符串。解析器决定该字符串是否为邮编。NIPOST API 决定邮编是否分配给建筑物。你自己的代码通过读取前缀决定哪个仓库或队列接收任务。这些决策均不使用模型的置信度作为证据。

_信任边界:模型提供有效载荷,边界后的所有决策和所有失败退出均属于确定性代码。_
邮编适合这种设计,因为其结构严格。它包含五个分段,每个分段都进一步缩小范围。
| 分段 | 示例 | 规则 | | --- | --- | --- | | 州 | EK | 两个字母 | | 地方政府区 | 01 | 两位数字,01到99 | | 区 | A03 | 三个字母或数字 | | 区域 | FK | 两个字母 | | 建筑单元 | 01 | 两位数字,01到99 |
大多数位置的错误字符会破坏代码可离线检查的规则,无需网络调用或模型参与。

_该编码的每个前缀都对应一个更小的区域,从州一直细化到单个建筑。_
如何构建门禁系统
门禁系统是一个函数,每个邮政编码在进入系统中的其他处理流程前,都会先经过这个门禁。parse 返回一个 Postcode 或错误值,因此类型检查器会强制你处理这两种情况。
from ng_postcode import Corrected, Postcode, parse, parse_lenient
def gate(text: str) -> Postcode | str:
match parse(text):
case Postcode() as code:
return code
case error:
match parse_lenient(text):
case Corrected(postcode=fixed):
return f"refused: {error}. Ask the user: did you mean {fixed}?"
case _:
return f"refused: {error}"
for text in ["ek 01 a03 fk 01", "100001", "EK-00-A03-FK-01", "EK-O1-A03-FK-01"]:
print(gate(text))EK-01-A03-FK-01
refused: expected 11 letters and digits, found 6
refused: invalid lga segment
refused: invalid lga segment. Ask the user: did you mean EK-01-A03-FK-01?第一个输入被接受并标准化。第二个输入是旧的六位编码,门禁系统会指出其问题所在。第三个输入的政府区域编号为 00,而格式中排除了这种情况。
在第四个输入中,字母 O 出现在只能是数字的位置,parse_lenient 可以推测出可能的意图。门禁系统仍然拒绝输入,并将建议作为问题返回给用户。修正只是一个推测,因此在用户确认前不应触达数据库或付费 API 调用。
如何通过 NIPOST 确认编码
门禁系统证明字符串的格式是正确的。NIPOST 是否将该编码分配给某个建筑是另一个问题,可以通过一次免费的一级查询来解答。在运行此代码前,请将密钥设置在 NG_POSTCODE_API_KEY 环境变量中。
import os
from ng_postcode import Postcode, parse
from ng_postcode.api import ApiError, lookup
from ng_postcode.client import Client, TransportError
def assigned(code: Postcode, client: Client) -> bool | str:
match client.send(lookup(code, level=1)):
case ApiError() | TransportError() as failure:
return f"unconfirmed: {failure}"
case found:
return found.valid
code = parse("EK-01-A03-FK-01")
assert isinstance(code, Postcode)
with Client(api_key=os.environ["NG_POSTCODE_API_KEY"]) as client:
print(assigned(code, client))FalseEK-01-A03-FK-01 是 NIPOST 文档中的示例,未分配给任何建筑,因此答案是 False。被拒绝的密钥或连接失败会返回以 unconfirmed 开头的字符串,这可以将失败的查询与 NIPOST 未分配的编码区分开。NIPOST 的测试密钥可能会将已分配的编码标记为未分配,因此在对 False 做出操作前请使用正式密钥。
如何根据前缀路由工作
因为每个段落都逐步缩小范围,所以代码的前缀命名一个区域:第一个段落是州,前两个是地方政府区域,依此类推,直到建筑物,这使得路由操作变成了字典查找。
from ng_postcode import Postcode, Segment, parse
DEPOTS = {"EK-01": "depot-a", "LA-05": "depot-b"} # 虚构名称
def depot(code: Postcode) -> str:
return DEPOTS.get(code.prefix(Segment.LGA), "national-sorting")
code = parse("EK-01-A03-FK-01")
assert isinstance(code, Postcode)
print(depot(code)) # depot-a路由表是您拥有的数据,可以进行审查。模型在此步骤中不参与,相同的代码始终会到达相同的分拣中心。未知的前缀会落入到命名的默认值,因此由于表格不完整而丢失工作的情况永远不会发生。
如果允许模型从自由文本地址中选择目的地,同一客户在不同日期可能会到达不同的分拣中心,因此请将此选择保留在代码中。
如何组合这些步骤
这三个步骤可以组合成一个函数。它接收模型提取的字符串,并返回分拣中心或下一步操作,且仅对已解析的输入调用NIPOST。
import os
from ng_postcode import Postcode, Segment, parse
from ng_postcode.api import ApiError, lookup
from ng_postcode.client import Client, TransportError
DEPOTS = {"EK-01": "depot-a", "LA-05": "depot-b"} # 虚构名称
def dispatch(text: str, client: Client) -> str:
code = parse(text)
if not isinstance(code, Postcode):
return f"请用户确认: {code}"
match client.send(lookup(code, level=1)):
case ApiError() | TransportError() as failure:
return f"稍后重试: {failure}"
case found if not found.valid:
return f"请用户确认: {code} 未分配到建筑物"
case _:
return DEPOTS.get(code.prefix(Segment.LGA), "national-sorting")
with Client(api_key=os.environ["NG_POSTCODE_API_KEY"]) as client:
for text in ["100001", "ek 01 a03 fk 01"]:
print(dispatch(text, client))请用户确认: 期望11个字母和数字,但找到6个
请用户确认: EK-01-A03-FK-01 未分配到建筑物每个返回值都指明下一步要发生的事情:向用户提问、重试或分拣中心。模型仅贡献 text 参数,其他不参与。要在问题中包含建议的更正,请使用前面部分中的 gate 方法替代 parse。
如何为代理提供带强制限制的工具
当代理确实需要直接处理邮政编码时,请为其提供带有书面合同的工具,就像您会为微服务编写文档一样。默认情况下,ng-postcode-mcp 服务器通过模型上下文协议暴露五个工具,并且可以与任何MCP客户端配合使用。

_作为MCP客户端看到的五个默认工具,在此处Glama的目录列表中显示。_
{
"mcpServers": {
"ng-postcode": {
"command": "uvx",
"args": ["ng-postcode-mcp"],
"env": { "NG_POSTCODE_API_KEY": "nipost_live_..." }
}
}
}| 工具 | 网络 | 需要密钥 | 费用 | | --- | --- | --- | --- | | validate_postcode | 无 | 否 | 免费 | | lookup_postcode | NIPOST | 是 | 免费,始终为级别1 | | autocomplete_postcode | NIPOST | 是 | 免费层级 | | find_postcode_at_location | NIPOST | 是 | 免费层级 | | resolve_address | NIPOST 和 geocoder | 是 | 免费层级 | | lookup_postcode_details | NIPOST | 是 | 每次调用消耗积分。仅当 NG_POSTCODE_MAX_LEVEL 为 2 或更高时才会注册 |
服务器强制执行三个限制,因此无论模型决定采取什么操作,这些限制都会生效:
- 五个默认工具均为只读模式。
- 密钥从环境变量中读取。它永远不会作为工具参数出现,因此模型无法看到它,也无法重复它。
- 付费查询属于第六个工具
lookup_postcode_details,服务器仅在将NG_POSTCODE_MAX_LEVEL提升至 2 或更高时才会注册该工具。
级别 2 会返回房屋地址并消耗积分。在默认服务器上,模型没有可以请求此功能的工具:lookup_postcode 仅接受级别 1,调用付费工具时会收到以下回复:
未知工具: lookup_postcode_details
``当你提升限制后,付费工具将不再显示只读提示,因此自动批准只读工具的客户端在消耗积分前仍会向你确认。服务器在单次运行中进行 25 次付费查询后也会停止,除非你将 NG_POSTCODE_MAX_PAID_CALLS 提升至更高。超过限制的请求会收到以下回复:
级别 3 超出此服务器的限制 2。级别 2+ 会消耗 NIPOST 积分;用户可以
将 NG_POSTCODE_MAX_LEVEL 提升以允许此操作。
``该权限由人员设置的配置文件控制,任何提示都无法更改它。
如何处理每种失败状态
假设模型会生成错误代码、密钥会过期、网络会失败。下表列出了每种状态、检测它的组件以及代理接收到的内容。每条消息均为服务器 0.6.2 版本的输出,记录于 2026 年 10 月 13 日。
| 状态 | 检测方式 | 返回内容 | | --- | --- | --- | | 旧的六位代码 | 解析器,离线 | expected 11 letters and digits, found 6 | | 段超出范围 | 解析器,离线 | invalid lga segment | | 类似拼写错误发送到查询 | 服务器,在任何网络调用之前 | 'EK-O1-A03-FK-01' is not a valid postcode: invalid lga segment. Did you mean EK-01-A03-FK-01? Confirm with the user first. | | 格式正确但从未分配 | NIPOST | "valid": false, "status": "not_found" | | 未配置密钥 | 服务器 | This tool needs a NIPOST API key | | 密钥被拒绝 | NIPOST | invalid_api_key (401), with where to create a new key | | 发送到 lookup_postcode 的层级超过 1 | 服务器 | Input should be less than or equal to 1 | | 发送到服务器的层级超过限制 | 服务器 | 上述的限制提示信息 | | 密钥不支持的层级 | NIPOST | level_not_granted (403) | | API 不可达 | HTTP 客户端 | Could not reach the NIPOST API: All connection attempts failed |

_检查按此顺序执行,前三个检查无需成本,因为它们在任何请求发送前就完成。_
第三行是类似拼写错误的情况。服务器发现了一个可能的更正,但拒绝使用它。它不会发起查询,其回复会提示模型需要与用户确认。一个自动更正代码并继续处理的系统,可能会返回用户从未提及的建筑物的真实地址。

_在任何请求发起之前,拼写错误的代码会被拒绝,只有在用户确认更正后,更正后的代码才会发送到 NIPOST。_
第四行是幻觉代码。模型可能生成一个符合格式规则但不属于任何建筑物的代码,离线验证会接受它。只有注册系统能区分这两种情况,因此在层级 1 上要查询所有相关代码。
HTTP 客户端通过三种方式保护每个请求。它为整个交互设置十秒的截止时间,因此停滞的连接无法占用代理的轮次。它不会遵循重定向,因为重定向会将你的密钥发送到其他主机。出于相同原因,它拒绝使用纯 http:// 的基础 URL,除非在你自己的机器上。
如何解码你不信任的回复
对 API 的响应也存在同样的疑虑。直接读取响应体的代理会找到一种方法来解释接收到的任何内容。该库将工作分开:lookup 以纯数据形式构建请求,而 decode 将状态和响应体转换为类型化结果或错误。你可以在不使用网络的情况下测试这一点。
from ng_postcode import Postcode, parse
from ng_postcode.api import ApiError, decode, lookup
code = parse("EK-01-A03-FK-01")
assert isinstance(code, Postcode)
request = lookup(code)
replies = [
(200, '{"data": {"postcode": "EK-01-A03-FK-01", "valid": false, "status": "not_found"}}'),
(200, '{"data": {"postcode": "EK-01-A03-FK-01"}}'),
(401, '{"error": {"code": "invalid_api_key", "message": "the provided API key is invalid or revoked"}}'),
(502, "<html>Bad Gateway</html>"),
]
for status, body in replies:
match decode(request, status, body):
case ApiError() as error:
print("error:", error)
case found:
print("assigned:", found.valid)assigned: False
error: malformed_response (200): unexpected data
error: invalid_api_key (401): the provided API key is invalid or revoked
error: malformed_response (502): not JSON: Expecting value: line 1 column 1 (char 0)在第二个响应中,响应体是有效的 JSON,状态码为 200,但 valid 字段缺失。一个宽松的解码器会将缺失字段视为 false,并报告该建筑不存在。这个解码器则报告响应格式错误,因此缺失字段永远不会成为对建筑的断言。该库的所有实现(包括 Rust、Python、JavaScript 和 Java)都通过相同的共享测试用例文件。
本方案无法解决的问题
一个有效且已分配的邮政编码仍可能是错误的。如果用户提供了邻居的编码,本文中所有检查都会通过。确认编码是否属于面前的人需要不同的控制方式,例如向用户展示该编码解析出的地址。
文本本身很少能唯一标识建筑:"Fabian Hotel 后方,NTA Road 旁" 描述的是一个区域,当证据仅支持到此程度时,resolve_address 工具会返回区域前缀并询问用户。对于精确的建筑,应要求提供定位图钉。
地址属于个人数据:二级查询会返回房屋地址,因此需根据尼日利亚数据保护法案处理结果,并在不需要更多信息时将查询级别限制为 1。
结论
模型是路径中最不可靠的组件,因此应赋予它最简单的任务:提取字符串。解析器可免费检查结构,注册表确认建筑是否存在。你自己的以前缀为键的表格决定工作流向。更正以问题形式返回给用户,而需要付费或暴露地址的权限则由人工控制的配置文件处理。
当模型学习新格式后,边界仍然有用。相同的设计也适用于地址系统、税码或账户号码的下一次变更,模型也会在后期学习这些变更。
代码在 github.com/Adeniyikayodee/ng-postcode 开源,采用 MIT 协议。为你的语言安装库:
pip install ng-postcode # Python
npm install ng-postcode-js # JavaScript and TypeScript
cargo add ng-postcode # Rust对于 Java,Maven 坐标为 io.github.adeniyikayodee:ng-postcode-api。如果库在某个案例上处理错误,请提交问题。
- * *
- * *
免费学习编程。freeCodeCamp 的开源课程已帮助超过 40,000 人成为开发者并获得工作。开始学习