Глава 23

Anthropic: Messages API и content blocks

Messages API у Anthropic stateless, и это определяет весь адаптер. Claude возвращает tool_use content blocks; приложение сохраняет весь assistant message, выполняет tools и отправляет один user message с соответствующими tool_result blocks. Никакого server-side id продолжения - историю держите вы.

TypeScript
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();
const model = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5";

type AnthropicState = {
  messages: any[];
  instructions: string;
  tools: ToolDefinition[];
};

function toAnthropicTool(tool: ToolDefinition) {
  return {
    name: tool.name,
    description: tool.description,
    input_schema: tool.parameters,
    strict: true
  };
}

Общий запрос дописывает assistant message к истории и нормализует content blocks в наш ProviderTurn.

TypeScript
async function request(state: AnthropicState): Promise<ProviderTurn> {
  const message = await client.messages.create({
    model,
    max_tokens: 2_048,
    system: state.instructions,
    tools: state.tools.map(toAnthropicTool),
    messages: state.messages
  });

  const nextState = {
    ...state,
    messages: [
      ...state.messages,
      { role: "assistant", content: message.content }
    ]
  };

  const calls = message.content
    .filter(function isToolUse(block: any) {
      return block.type === "tool_use";
    })
    .map(function normalize(block: any): ToolCall {
      return { id: block.id, name: block.name, arguments: block.input };
    });

  if (calls.length > 0) return { kind: "tool_calls", calls, state: nextState };

  const text = message.content
    .filter(function isText(block: any) { return block.type === "text"; })
    .map(function takeText(block: any) { return block.text; })
    .join("");
  return { kind: "text", text, state: nextState };
}

А возврат результатов складывает tool_result blocks в один user message.

TypeScript
async function resume(rawState: unknown, results: ToolResult[]) {
  const state = anthropicStateSchema.parse(rawState);
  const toolResults = results.map(function toToolResult(result) {
    return {
      type: "tool_result" as const,
      tool_use_id: result.callId,
      content: JSON.stringify({ ok: !result.isError, value: result.output }),
      is_error: result.isError
    };
  });

  return request({
    ...state,
    messages: [...state.messages, { role: "user", content: toolResults }]
  });
}

Две детали здесь легко нарушить по инерции. Первая - про модель.

Claude Sonnet 5. Модель использует adaptive thinking по умолчанию. Не переносите старую настройку manual extended thinking и не задавайте non-default temperature, top_p или top_k: актуальная migration guide сообщает, что Sonnet 5 отклоняет такие параметры.

Вторая - про форму ответа с результатами.

Parallel results одним сообщением. Если Claude вызвал несколько tools, верните по одному tool_result на каждый tool_use в одном следующем user message. Не вставляйте текст перед result blocks.

Прямой Messages API - стабильная точка понимания. Но у Anthropic есть и удобный автоматический цикл - Tool Runner.

Проверка знаний

Как обращаться с temperature и manual thinking для Claude Sonnet 5?

Ссылки