Claude Sonnet 5.5 接进现有问答链路,先用一次最小请求验通字段

文章导读
第一次把 Claude Sonnet 5.5 接进现有问答链路,报错往往出现在返回解析这一步:日志里能看到请求发出去了,但代码在 json() 之后取字段时抛异常。这类报错通常不是模型不可用,而是鉴权没过、请求体字段名与服务端约定不一致、或返回结构本身不是你以为的那一种。相对稳妥的顺序是:不碰业务代码,先用一次最小请求把「密钥能过、请求体被接受、返回结构长什么样」这三件事分别看清,再改问答链路里的
📋 目录
  1. Ⅰ 把模型标识和密钥放进环境变量
  2. Ⅱ 写最小请求骨架,只打印状态码和原始返回
  3. Ⅲ 逐字段核对请求体:角色、内容、输出上限
  4. Ⅳ 把返回体解析成结构化字段并处理非预期结构
  5. Ⅴ 落一条成功日志和一条失败日志
A A

第一次把 Claude Sonnet 5.5 接进现有问答链路,报错往往出现在返回解析这一步:日志里能看到请求发出去了,但代码在 json() 之后取字段时抛异常。这类报错通常不是模型不可用,而是鉴权没过、请求体字段名与服务端约定不一致、或返回结构本身不是你以为的那一种。相对稳妥的顺序是:不碰业务代码,先用一次最小请求把「密钥能过、请求体被接受、返回结构长什么样」这三件事分别看清,再改问答链路里的调用。

适用场景:已拿到模型标识和密钥,首次接入或换了接入方式后解析失败。操作动作:密钥与模型标识走环境变量;先用一次最小请求只打印状态码和原始返回,不解析;确认返回结构后再逐字段取值。验证方式:脱敏打印模型标识、打印原始返回、给返回体加存在性判断。风险边界:字段名与鉴权头以实际文档为准,不同网关可能有差异;原始返回里若含敏感内容,不要整段进日志。

把模型标识和密钥放进环境变量

硬编码密钥进代码库,后面清理成本很高。先把两项配置放到运行时环境里,再确认进程真的读到了——很多「鉴权失败」其实是容器、systemd 或 IDE 没把变量传进去,代码读到的是空字符串。

export CLAUDE_MODEL='your-model-id'
export CLAUDE_API_KEY='your-key'
export CLAUDE_BASE_URL='https://your-endpoint'   # 按实际接入地址替换

在代码里读,读不到就直接中断,而不是带着空值去发请求:

import os

model = os.getenv('CLAUDE_MODEL', '').strip()
api_key = os.getenv('CLAUDE_API_KEY', '').strip()

if not model or not api_key:
    raise SystemExit('环境变量未读到:检查部署环境是否注入了 CLAUDE_MODEL / CLAUDE_API_KEY')

确认模型标识时只打印脱敏结果,避免把整串标识和密钥写进终端历史或日志:

python -c "import os;m=os.getenv('CLAUDE_MODEL','');print('model=', (m[:4]+'***'+m[-4:]) if len(m)>10 else 'EMPTY_OR_TOO_SHORT')"

如果这条命令在本地能打印出值、在部署环境里打印 EMPTY_OR_TOO_SHORT,问题就在环境注入环节,不用往下查请求体。

写最小请求骨架,只打印状态码和原始返回

这一步刻意不做 JSON 解析。先看清服务端到底回了什么,比在解析器里猜字段要快得多。请求方法、路径、认证头按你的接入方式替换,字段名以实际文档为准。

import os, time, requests

url = os.getenv('CLAUDE_BASE_URL', '').rstrip('/') + '/v1/messages'   # 路径以实际文档为准
headers = {
    'x-api-key': os.getenv('CLAUDE_API_KEY', ''),        # 也可能是 Authorization: Bearer
    'anthropic-version': '2023-06-01',                   # 以实际文档为准,不需要就删掉
    'content-type': 'application/json',
}
body = {
    'model': os.getenv('CLAUDE_MODEL', ''),
    'max_tokens': 64,
    'messages': [{'role': 'user', 'content': 'ping'}],
}

t0 = time.time()
r = requests.post(url, headers=headers, json=body, timeout=30)
print('status =', r.status_code)
print('elapsed_ms =', int((time.time() - t0) * 1000))
print('raw_body =', r.text[:800])

看结果时分三种情况:状态码 401/403,先回头查密钥和环境变量;400 类错误,通常请求体字段名或类型不对,原始返回里一般会指出哪个字段;200 但 raw_body 结构和你预期的不一样,就是返回结构理解有偏差。此时不要急着改业务代码。

Claude Sonnet 5.5 接进现有问答链路,先用一次最小请求验通字段

逐字段核对请求体:角色、内容、输出上限

解析失败经常发生在解析之前——请求体本身就没被正确接受,返回的是错误对象,代码却按成功结构去取字段。把下面几项逐个对一遍,标「需替换」的地方用实际取值填。

  • model:模型标识,需替换成你环境变量里的值。常见错误是写成展示名、带空格、或写成上个版本的标识。
  • max_tokens:单次输出上限。常见错误是写成 maxTokens、max_new_tokens 这类驼峰或旧命名,服务端会忽略或直接报错。
  • messages:应是有序数组,不是字符串。常见错误是直接写 'content': 'ping' 替代整个数组。
  • messages[].role:取值通常是 user 和 assistant。把系统提示写成 role: 'system' 塞进数组,在部分实现里是非法值,需要放到单独的顶层字段。
  • messages[].content:纯字符串最省事;如果传数组,元素需要是带类型标记的对象,类型名写错同样会被拒。

核对方式是改一个字段、发一次最小请求,看状态码和原始返回的变化,而不是一次性改完再排查。

把返回体解析成结构化字段并处理非预期结构

最小请求验通后,解析才算有依据。取值前先判断存在性,缺字段时降级成空结果并记日志,不要让一条异常响应把整个问答链路打挂。

try:
    data = r.json()
except ValueError:
    data = {}

text = ''
stop_reason = data.get('stop_reason') or data.get('stopReason') or 'unknown'
usage = data.get('usage') or {}

blocks = data.get('content')
if isinstance(blocks, list):
    parts = [b.get('text', '') for b in blocks
             if isinstance(b, dict) and b.get('type') == 'text']
    text = ''.join(parts).strip()
elif isinstance(blocks, str):
    text = blocks.strip()

if not text:
    text = ''   # 降级:交给上游走兜底话术,不要抛异常中断会话

降级策略要和产品侧对齐:返回空文本时是重试一次、切兜底回复,还是提示稍后再试,取决于你的问答链路。无论选哪种,都要把原始返回留一点在日志里,否则下次没法判断是网络问题还是结构问题。

落一条成功日志和一条失败日志

跑通之后留一条可比对的日志,之后换模型标识、改提示词、调输出上限时,才有回归基线。日志里建议固定包含:模型标识(脱敏)、请求耗时、状态码、停止原因、用量。

import logging

logger = logging.getLogger('qa.claude')

if r.status_code == 200 and text:
    logger.info('claude_ok model=%s elapsed_ms=%s status=%s stop=%s usage=%s',
                safe_model, elapsed_ms, r.status_code, stop_reason, usage)
else:
    logger.warning('claude_fail model=%s elapsed_ms=%s status=%s stop=%s usage=%s raw=%s',
                   safe_model, elapsed_ms, r.status_code, stop_reason, usage, r.text[:300])

成功日志和失败日志用同一组字段,改动前后的两类记录才放得到一起看。停止原因如果是长度截断,通常意味着输出上限偏小;如果字段取不到值,先回到返回体结构本身确认,而不是直接调参数。