Reliability and Routing2026年9月22日Flatkey Team

API 错误 400 “Text Content Blocks Must Be Non-Empty”:原因与 5 个修复方法

通过有效载荷检查、清理器代码、SDK 示例和 QA 步骤,修复 Anthropic 请求中的 API 错误 400 “Text Content Blocks Must Be Non-Empty”。

API 错误 400 “Text Content Blocks Must Be Non-Empty”:原因与 5 个修复方法

API 错误 400 "Text Content Blocks Must Be Non-Empty":原因与 5 个修复方法 指的是 Anthropic Messages API 请求中至少包含一个 text 值为空的文本块。该请求在消息层面看起来可能是有效的,但服务提供方会在生成前拒绝它,因为文本内容块必须至少包含一个字符。

快速修复很简单:移除空文本块,在构建请求之前裁剪仅包含空白字符的用户输入,并且绝不要发送诸如 {"type":"text","text":""} 这样的占位块。更难的部分是找出这些块是在哪里引入的。在生产应用中,它们通常来自聊天 UI 草稿、空的检索片段、Markdown 清理器、模板变量、流式转写缓冲区,或是在知道是否存在文本之前就先构建内容数组的多模态适配器。

使用本指南来调试该错误、修补请求构建器,并添加一个预检保护,以免同样的 400 再次上线。

快速回答:API 错误 400 "Text Content Blocks Must Be Non-Empty"

Anthropic 接受消息 content 为普通字符串或带类型的内容块数组。在数组形式中,文本块如下所示:

{
  "type": "text",
  "text": "Summarize this support ticket."
}

这会失败,因为 text 字段为空:

{
  "type": "text",
  "text": ""
}

如果你的应用将仅包含空白字符的值规范化为空字符串,实际中这也会失败:

{
  "type": "text",
  "text": "   "
}

最安全的规则是:

  1. 在构建 Anthropic 请求之前裁剪文本值。
  2. 丢弃裁剪后文本为空的文本块。
  3. 如果某条消息不再包含任何内容块,就不要发送该消息。
  4. 记录清理后的 payload 结构,但不要记录私密提示文本。
  5. 针对空字符串、空白字符、null 和空检索结果添加单元测试。

这就是针对 API 错误 400 "Text Content Blocks Must Be Non-Empty":原因与 5 个修复方法 的实用修复方案。

为什么会发生这个错误

Anthropic 的 Messages API 使用结构化的对话轮次。每个输入消息都有一个 rolecontentcontent 的值可以是单个字符串,也可以是诸如文本块和图片块之类的块数组。官方 Messages API 参考文档将字符串内容描述为单个文本块的简写,并在文本块上将 text 列为 minLength: 1

Anthropic 的错误参考将 HTTP 400 归类为 invalid_request_error:即请求格式或内容存在问题。因此,这不是速率限制、认证失败、服务提供方中断,也不是模型质量问题,而是一个请求验证问题。

对于 AI 产品团队来说,运维层面的教训很重要:重试同一个请求不会有帮助。你需要先修复 payload,再进行重试。

五个常见原因

1. 空的聊天输入被发送到 API

最常见的路径是聊天输入框允许用户提交空草稿,或者草稿在裁剪后变为空。

错误请求:

{
  "model": "claude-sonnet-5",
  "max_tokens": 512,
  "messages": [
    {
      "role": "user",
      "content": [
        { "type": "text", "text": "" }
      ]
    }
  ]
}

在 API 调用之前先修复它:

const input = userInput.trim();

if (!input) {
  throw new Error("调用 Anthropic 之前消息文本是必填项。");
}

const messages = [
  {
    role: "user",
    content: input
  }
];

在 UI 中使用产品级的校验提示。不要让后端从供应商的 400 错误中才发现空提示词。

2. 检索加入了空块

RAG 流水线通常会把检索到的文档映射到提示词各部分。如果某个检索结果有空片段、被移除的 HTML 正文,或者失败的 OCR 字段,适配器就可能创建一个空的文本块。

有问题的适配器:

const content = retrievedDocs.map((doc) => ({
  type: "text",
  text: doc.cleanedText
}));

更安全的适配器:

const content = retrievedDocs
  .map((doc) => (doc.cleanedText ?? "").trim())
  .filter(Boolean)
  .map((text) => ({ type: "text", text }));

如果模型需要源上下文,也要保留丢弃块的数量。如果所有检索到的块都是空的,就应停止并返回检索错误,而不是发送空提示词。

3. 模板变量渲染为空

提示词模板是导致 API 错误 400 "Text Content Blocks Must Be Non-Empty":原因与 5 个修复方法 的另一个常见原因。模板在代码里看起来是有内容的,但运行时可能渲染出一个空区域:

const prompt = `
Customer message:
${customerMessage}
`;

如果 customerMessageundefinednull,或者清理后为空,那么最终提示词可能无效或为空。

使用显式必填字段:

function requiredText(name: string, value: unknown): string {
  const text = String(value ?? "").trim();
  if (!text) {
    throw new Error(`缺少必填的提示词字段:${name}`);
  }
  return text;
}

const prompt = `Customer message:\n${requiredText("customerMessage", customerMessage)}`;

这会把模糊的供应商错误转化为带有缺失字段名称的本地应用错误。

4. 多模态构建器添加了一个占位文本块

构建图文流程的团队有时会用一个占位文本块来初始化内容数组,然后稍后再填充。如果文本是可选的,而最终没有文本传入,占位符就会一直保持为空。

有问题的模式:

[
  { "type": "text", "text": "" },
  {
    "type": "image",
    "source": {
      "type": "base64",
      "media_type": "image/png",
      "data": "..."
    }
  }
]

更安全的模式:

const content: Array<Record<string, unknown>> = [];

const instruction = optionalInstruction.trim();
if (instruction) {
  content.push({ type: "text", text: instruction });
}

content.push({
  type: "image",
  source: imageSource
});

仅在相应内容存在时才构建块。不要将空文本块用作分隔符。

5. 消息历史压缩留下空白轮次

长期运行的助手通常会压缩或总结之前的轮次。如果压缩步骤移除了消息正文,但在历史记录中保留了该轮次,那么你的请求可能会包含一条空白的 assistant 或 user 消息。

失败示例:

{
  "role": "assistant",
  "content": [
    { "type": "text", "text": "" }
  ]
}

在每次调用之前使用历史清理器:

type TextBlock = { type: "text"; text: string };
type Message = { role: "user" | "assistant"; content: string | TextBlock[] };

function sanitizeMessages(messages: Message[]): Message[] {
  return messages.flatMap((message) => {
    if (typeof message.content === "string") {
      const text = message.content.trim();
      return text ? [{ ...message, content: text }] : [];
    }

    const content = message.content
      .map((block) => ({ ...block, text: block.text.trim() }))
      .filter((block) => block.type !== "text" || block.text.length > 0);

    return content.length ? [{ ...message, content }] : [];
  });
}

然后在调用 API 之前断言至少保留了一条消息。

可复制的预检验证器

在最终网络边界附近使用请求预检验证器。即使上游 UI、模板、RAG 或 memory 模块漏掉了空块,它也能将其捕获。

type ContentBlock =
  | { type: "text"; text?: unknown }
  | { type: string; [key: string]: unknown };

type AnthropicMessage = {
  role: "user" | "assistant";
  content: string | ContentBlock[];
};

export function validateAnthropicMessages(messages: AnthropicMessage[]) {
  const cleaned = messages.flatMap((message, messageIndex) => {
    if (typeof message.content === "string") {
      const text = message.content.trim();
      return text ? [{ ...message, content: text }] : [];
    }

    const content = message.content.flatMap((block, blockIndex) => {
      if (block.type !== "text") return [block];

      const text = String(block.text ?? "").trim();
      if (!text) {
        console.warn("Dropped empty Anthropic text block", {
          messageIndex,
          blockIndex
        });
        return [];
      }

      return [{ ...block, text }];
    });

    return content.length ? [{ ...message, content }] : [];
  });

  if (!cleaned.length) {
    throw new Error("Anthropic request has no non-empty message content.");
  }

  return cleaned;
}

这是一种有意采取保守策略的做法。它会移除空文本块,保留非文本块,移除空消息,并且如果没有可用的消息内容,则拒绝调用模型。

Python 版本

如果你的后端是 Python,请使用相同的边界检查:

def sanitize_anthropic_messages(messages):
    cleaned_messages = []

    for message_index, message in enumerate(messages):
        content = message.get("content")

        if isinstance(content, str):
            text = content.strip()
            if text:
                cleaned_messages.append({**message, "content": text})
            continue

        if isinstance(content, list):
            cleaned_blocks = []

            for block_index, block in enumerate(content):
                if block.get("type") != "text":
                    cleaned_blocks.append(block)
                    continue

                text = str(block.get("text") or "").strip()
                if text:
                    cleaned_blocks.append({**block, "text": text})
                else:
                    print(
                        "Dropped empty Anthropic text block",
                        {"message_index": message_index, "block_index": block_index},
                    )

            if cleaned_blocks:
                cleaned_messages.append({**message, "content": cleaned_blocks})

    if not cleaned_messages:
        raise ValueError("Anthropic 请求没有非空消息内容。")

    return cleaned_messages

保持日志元数据的结构化。除非你的隐私政策和调试流程明确允许,否则不要记录原始用户提示、客户文档或私有检索文本。

调试清单

API 错误 400 "Text Content Blocks Must Be Non-Empty":原因与 5 个修复方法出现在生产环境中时,请按以下顺序排查:

检查项 需要检查的内容 修复方法
UI 输入 空的或仅包含空白字符的用户草稿 在存在修剪后的文本之前阻止提交
提示模板 必需变量渲染为空 按名称验证必填字段
RAG 片段 空的 cleanedText、OCR 结果或 markdown 正文 过滤片段,如果所有上下文都为空则失败
多模态请求 在图像/文件块之前的占位文本块 仅在有文本时推送文本块
历史压缩 摘要后空白的用户或助手轮次 清理最终消息列表
网络边界 最终有效负载仍包含 text: "" 添加预检验证器和单元测试

最终的网络有效负载是事实来源。如果你的日志显示没有空文本块,请确认 SDK 在序列化过程中没有将 nullundefined 或空数组转换为文本块。

要添加的单元测试

至少为以下情况添加测试:

const cases = [
  { name: "纯空字符串", content: "" },
  { name: "纯空白字符串", content: "   " },
  { name: "空文本块", content: [{ type: "text", text: "" }] },
  { name: "缺少 text 字段", content: [{ type: "text" }] },
  { name: "text 字段为 null", content: [{ type: "text", text: null }] },
  { name: "有效文本块", content: [{ type: "text", text: "Hello" }] }
];

你的期望行为应该明确:

  • 仅包含文本的空消息会在本地被移除或拒绝。
  • 有效文本在去除首尾空白后仍然保留。
  • 非文本内容块会被保留。
  • 没有可用内容的请求会在调用 Anthropic 之前抛出错误。
  • 抛出的错误应标识你的应用边界,而不只是提供商的响应。

Flatkey 的作用

如果你的团队通过 Flatkey 路由 Claude 流量,请保持相同的 Anthropic 负载规范。Flatkey 的 Anthropic SDK 指南在 Anthropic SDK 路径中使用 base_url="https://router.flatkey.ai",而 OpenAI 兼容 API 则对 chat-completions 风格请求使用 https://router.flatkey.ai/v1。请使用与你的客户端和端点形式相匹配的路由。

对于这个错误,Flatkey 最适合作为修复周围的操作层:

  • 保留一个地方来验证请求是否到达网关。
  • 在成功重试后比较请求状态和使用情况证据。
  • 将一个小型冒烟测试与用户的完整提示分开。
  • 避免在同一个适配器中混用 Anthropic 格式请求和 OpenAI 兼容请求。

如果你正在为 Claude 工作负载选择路由,请阅读 Claude API Proxy vs Multi-Model Router。如果你正在标准化工程师如何进行第一次安全调用,请将 Flatkey API quickstart 放在附近。对于更广泛的生产检查,请将其与 AI routing API metrics 以及 AI model catalog guide 搭配使用。

不要这样做

不要用盲目重试来解决 API 错误 400 "Text Content Blocks Must Be Non-Empty":原因与 5 个修复方法。提供商是在告诉你请求格式有误。

避免以下反模式:

反模式 为什么会失败
重试相同的负载 确定性的校验错误会一直失败
"." 替换空文本 这会掩盖上游数据丢失,并且可能改变模型行为
发送空的 assistant 回合 它会污染历史记录,并可能破坏响应续接
为了调试而记录完整提示词 它可能暴露客户数据或密钥
只修复 UI 后端任务、RAG、webhook 和 agent 循环仍然可能创建空块

持久的修复方式是在生产者边界和最终 API 边界同时验证内容。

常见问题

这是 Anthropic 方面的故障吗?

不是。空文本块导致的 400 invalid_request_error 是请求校验问题。请检查您的应用发送的负载。

我可以发送普通字符串,而不是文本块数组吗?

可以。Anthropic 的 Messages API 允许消息 content 为字符串,文档将其描述为一个文本块的简写。当您只需要简单文本时,请使用字符串;当您需要多个块或多模态输入时,请使用数组。

空白字符是否应视为非空?

请在您自己的验证器中将仅包含空白字符的文本视为空。即使某个提供方接受了它,它也不是有用的提示内容,通常还表明存在 UI、模板或检索方面的 bug。

仅包含图片的消息可以吗?

多模态请求不需要空的文本占位符。如果您包含图片块,请直接构建图片块;只有在您确实有指令文本时,才添加文本块。

我应该记录什么?

记录消息数量、内容块类型、块索引、模型、端点、路由、状态码,以及可用时的请求 ID。除非您团队的隐私规则允许,否则避免记录完整的提示文本。

官方参考资料