大模型接口返回200还不够:用Python给聊天响应做结构校验

举报
yd_243677465 发表于 2026/11/03 00:21:51 2026/11/03
【摘要】 以非流式纯文本聊天接口为例,用Python标准库分层校验HTTP状态、JSON和choices/message/content结构,并用12个自建用例验证错误分支及日志最小化。

调试聊天模型接口时,常见的写法是请求结束后直接读取 choices[0].message.content。正常返回时很简洁,但HTTP成功与“能给业务返回文本”之间,还隔着一层响应结构校验。这里整理一个只用Python标准库的练习,把这一层单独写出来,用模拟数据验证。

本文代码面向一种明确的业务约定:非流式、第一候选项、纯文本聊天响应。它不是某个云服务所有响应类型的完整规范,也没有进行真实在线推理或模型效果评测。示例全部使用自建数据,不需要API Key,不创建云资源。

一、先明确三层成功条件

第一层是传输调用成功。网络超时、证书问题和连接失败属于调用层,应该由HTTP客户端处理。第二层是HTTP状态符合预期。本文的业务约定只接受200;401、429等状态不进入文本提取。第三层才是响应结构满足当前调用方的需求:顶层对象、非空choices列表、合法的message对象,以及非空字符串content。

这三层需要分开。只判断状态码,不能保证列表里有元素;只捕获KeyError,也不能覆盖空列表的IndexError以及结构类型不匹配。与其让异常发生在很深的下标表达式中,不如在业务边界给出短小、稳定的错误码。

二、把校验写成独立函数

下面的extract_text不发网络请求。调用方传入状态码和响应正文,它只返回文本或抛出ResponseContractError。返回值保留原始字符串,包括有效文本两侧的空白;strip只用来识别全空白内容。

错误消息只包含固定原因与HTTP状态,不附带原始正文。正文可能包含用户问题、模型回答或服务诊断信息,直接拼到异常里会让它们进入后续日志。此处采用信息最少的默认行为,生产环境可另外记录经过审查的请求标识。

代码中对每一层分别检查类型,因而能把“缺字段”和“字段存在但类型不对”收敛到可理解的业务错误。先验证整个对象,再访问子对象,也使后续维护者更容易看清每一个分支。

"""Validate a non-streaming text chat response without logging its body."""
import json


class ResponseContractError(ValueError):
    pass


def extract_text(status, body):
    if status != 200:
        raise ResponseContractError(f"http_status:{status}")
    try:
        data = json.loads(body)
    except (ValueError, TypeError, UnicodeError):
        raise ResponseContractError("invalid_json") from None
    if not isinstance(data, dict):
        raise ResponseContractError("root_not_object")
    choices = data.get("choices")
    if not isinstance(choices, list) or not choices:
        raise ResponseContractError("choices_empty_or_invalid")
    first = choices[0]
    if not isinstance(first, dict):
        raise ResponseContractError("choice_not_object")
    message = first.get("message")
    if not isinstance(message, dict):
        raise ResponseContractError("message_not_object")
    content = message.get("content")
    if not isinstance(content, str) or not content.strip():
        raise ResponseContractError("text_missing_or_empty")
    return content


def run_demo():
    good = {"choices": [{"message": {"content": "你好"}}]}
    cases = [
        ("success", 200, json.dumps(good), "你好"),
        ("http401", 401, "PRIVATE_BODY", "http_status:401"),
        ("non_json", 200, "PRIVATE_BODY", "invalid_json"),
        ("array_root", 200, "[]", "root_not_object"),
        ("missing_choices", 200, "{}", "choices_empty_or_invalid"),
        ("empty_choices", 200, '{"choices":[]}', "choices_empty_or_invalid"),
        ("wrong_choices", 200, '{"choices":{}}', "choices_empty_or_invalid"),
        ("wrong_choice", 200, '{"choices":[null]}', "choice_not_object"),
        ("missing_message", 200, '{"choices":[{}]}', "message_not_object"),
        ("null_content", 200, '{"choices":[{"message":{"content":null}}]}', "text_missing_or_empty"),
        ("blank_content", 200, '{"choices":[{"message":{"content":"  "}}]}', "text_missing_or_empty"),
        ("tool_only", 200, '{"choices":[{"message":{"tool_calls":[]}}]}', "text_missing_or_empty"),
    ]
    for name, status, body, expected in cases:
        try:
            actual = extract_text(status, body)
        except ResponseContractError as exc:
            actual = str(exc)
        assert actual == expected, (name, actual, expected)
        assert "PRIVATE_BODY" not in actual
        print(name, "OK")
    print(f"passed {len(cases)} cases")


if __name__ == "__main__":
    run_demo()

将文件保存为chat_response_contract.py,执行python chat_response_contract.py即可运行。代码没有第三方依赖,也不包含任何真实凭证。

三、12个用例覆盖了什么

本地使用Python3.11实际运行,输出最后一行为passed 12 cases。用例包括正常文本、401、非JSON正文、数组顶层、缺失choices、空choices、错误choices类型、空候选项、缺失message、null内容、空白内容以及仅工具调用的消息。

每个用例不仅检查是否报错,还检查具体错误码。比如空choices不应该产生模糊的“程序出错”,而应该进入choices_empty_or_invalid分支。测试中的PRIVATE_BODY是专门放入的标记,断言确保返回结果与错误文字没有包含这段正文。

这只能说明这12个自建输入的处理符合函数约定,不能证明真实服务始终返回这种结构,也不能证明所有敏感内容都不可能在其他日志环节泄露。特别是HTTP客户端的调试日志仍需单独检查。

四、工具调用为什么也会被拒绝

有些合法响应的重点是tool_calls而不是content。对支持工具调用的智能体来说,这需要进入工具调度分支;对本文这个“必须取得非空文本”的函数来说,它不满足约定,所以会被明确拒绝。这是调用方的选择,不应因此认定服务返回错误。

同样,流式SSE、多模态内容数组、多候选项合并、拒绝响应、截断标志和finish_reason等都需要根据实际协议增加处理。不要把这段纯文本示例直接当作通用SDK。尤其不能把空内容擅自转为成功,或者用空字符串掩盖协议变化。

五、接回真实客户端时再补哪些环节

接入网络请求时,应保留默认的TLS证书校验,从环境变量读取密钥,为客户端配置适当超时,并对网络异常单独处理。具体超时含义要以所用HTTP库的文档为准,连接超时和读取超时也不等同于整个业务流程的硬截止时间。

对话请求是否重试,需要考虑服务计费和幂等语义。收到错误后自动重试多次,可能产生重复调用;本文没有实现重试。可先让业务显示短错误码,再由操作者判断是否继续。响应体大小限制、日志脱敏和请求标识关联也应在真实集成时补齐。

这次练习的目的,是先用没有网络和凭证的固定输入,把成功与失败分支做清楚。之后再接真实模型时,就能更容易分辨问题是在网络层、HTTP状态层,还是响应内容层。

本文示例与文字由AI辅助编写,示例已按文中范围完成本地验证。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。