Anthropic Messages 接口

Anthropic 的接口和 OpenAI 的 Chat Completions 思路相近,核心也是一份按时间顺序排列的 messages 数组,但有几处关键差异,下面逐条配上 JSON 直观对比。

OpenAI 是把 system 当成 messages 数组里的一种 role;Anthropic 把系统提示放在请求体顶层的 system 字段,messages 里只允许 userassistant 两种角色。


OpenAI:

{
    "model": "gpt-4o",
    "messages": [
        { "role": "system", "content": "你是一个翻译助手" },
        { "role": "user",   "content": "hello" }
    ]
}

Anthropic:

{
    "model": "claude-sonnet-5",
    "system": "你是一个翻译助手",
    "messages": [
        { "role": "user", "content": "hello" }
    ]
}

OpenAI 里 max_tokens 可以省略走默认值,Anthropic 这里是请求体的必填项,不传会直接报错 422 状态码。

OpenAI:

{
    "model": "gpt-4o",
    "messages": [ { "role": "user", "content": "hello" } ]
}

Anthropic(少了 max_tokens 会 422):

{
    "model": "claude-sonnet-5",
    "max_tokens": 1024,
    "messages": [ { "role": "user", "content": "hello" } ]
}

Anthropic 接口的角色只有两种:

  • user:用户的话(也承载 tool_result)。
  • assistant:模型之前的回答(含正文文本、深度思考块、工具调用块等)。

系统设定走顶层的 system 字段,不要塞进 messages


工具结果回填成 user 消息。OpenAI 是用专门的 role:"tool" 回填;Anthropic 没有这个角色,工具结果以 tool_result 内容块的形式塞进一条 user 消息里,靠 tool_use_id 跟之前的 tool_use 块配对。

OpenAI:

[
    { "role": "assistant", "tool_calls": [ { "id": "call_1", "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" } } ] },
    { "role": "tool", "tool_call_id": "call_1", "content": "晴,25℃" }
]

Anthropic:

[
    { "role": "assistant", "content": [ { "type": "tool_use", "id": "toolu_1", "name": "get_weather", "input": { "city": "北京" } } ] },
    { "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "toolu_1", "content": "晴,25℃" } ] }
]


内容是「块」而不是字符串,无论请求还是响应,一条消息的 content 都可以是一个数组,里面是 textimagetool_usetool_resultthinking 等带类型的「内容块」。SDK 把这些块建模成联合类型(OneOf),用 TryPickXxx 来拆。

OpenAI(content 多为字符串,工具调用另放在 tool_calls):

{ "role": "assistant", "content": "今天天气不错" }

Anthropic(content 是块数组,文字、图片、工具调用都在里面):

{
    "role": "assistant",
    "content": [
        { "type": "text", "text": "今天天气不错" },
        { "type": "tool_use", "id": "toolu_1", "name": "get_weather", "input": { "city": "北京" } }
    ]
}


创建客户端

官方 nuget 包地址:https://www.nuget.org/packages/Anthropic

AnthropicClient 默认从环境变量读取配置:

环境变量说明
ANTHROPIC_API_KEYAPI Key,请求头以 x-api-key 发送
ANTHROPIC_AUTH_TOKENBearer Token,请求头以 Authorization: Bearer 发送
ANTHROPIC_BASE_URL服务地址,默认 https://api.anthropic.com


最省事的用法是不传任何参数,SDK 会自动读取 ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL 等环境变量。

using Anthropic;
using Anthropic.Models.Messages;

AnthropicClient client = new();


如果想显式指定 API Key、自定义网关地址(比如走本地代理或第三方兼容服务),就传一个 ClientOptions(注意类型名是 Anthropic.Core.ClientOptions,没有 AnthropicClientOptions):

AnthropicClient client = new(new ClientOptions
{
    ApiKey = "sk-ant-xxx",
    BaseUrl = "http://127.0.0.1:1234",
    HttpClient = new HttpClient(new LoggingHandler()),
});

MessageCreateParams parameters = new()
{
    MaxTokens = 1024,
    System = "你是一个数学计算器",
    Messages =
    [
        new()
        {
            Role = Role.User,
            Content = "1+1=?",
        },
    ],
    Model = "qwen/qwen3.5-9b",
};


var response = await client.Messages.Create(parameters);

foreach (ContentBlock block in response.Content)
{
    if (block.TryPickText(out TextBlock? text))
    {
        Console.WriteLine(text.Text);
    }
}

ClientOptions 还有几个常用项:HttpClient / Handlers(自定义 HTTP 处理管道,可用于打日志)、MaxRetries(默认 2)、Timeout(默认 10 分钟)、ResponseValidation(是否校验响应)。


请求参数

一次请求由 MessageCreateParams 描述,几个核心字段:

参数类型说明
Modelstring/Model 枚举模型名,如 claude-sonnet-5。支持从 string 隐式转换
MessagesIReadOnlyList<MessageParam>必填,对话历史,角色只能是 user/assistant
MaxTokenslong必填,输出 token 上限
SystemstringList<TextBlockParam>系统提示,顶层字段,不在 Messages
Temperaturedouble?采样温度(较新模型上已废弃)
TopP / TopKdouble? / long?核采样参数(较新模型上已废弃)
StopSequencesIReadOnlyList<string>自定义停止字符串
ThinkingThinkingConfigParam扩展思考(深度思考)配置
ToolsIReadOnlyList<ToolUnion>工具定义
ToolChoiceToolChoice工具选择策略(Auto/Any/Tool/None)

Model 在 SDK 里其实是个枚举(Model.ClaudeSonnet5Model.ClaudeOpus4_6Model.ClaudeHaiku4_5 等),但因为属性类型 ApiEnum<string, Model> 提供了从 string 的隐式转换,所以直接写字符串 "claude-sonnet-5" 也行,对接非官方兼容服务时更灵活。

最简单的请求:

MessageCreateParams parameters = new()
{
    MaxTokens = 1024,
    Model = "claude-sonnet-5",
    Messages =
    [
        new() { Role = Role.User, Content = "1+1=?" },
    ],
};

Message message = await client.Messages.Create(parameters);

注意 MessageParam.Content 是个联合类型,既能直接给字符串,也能给一个 ContentBlockParam 列表(用来混排文字、图片、工具结果),SDK 都提供了从 string 的隐式转换,所以日常纯文本对话写起来很简洁。


非流式对话

Anthropic 的非流式对话很简单。

AnthropicClient client = new(new ClientOptions
{
    ApiKey = "sk-ant-xxx",
    BaseUrl = "http://127.0.0.1:1234",
    HttpClient = new HttpClient(new LoggingHandler()),
});

MessageCreateParams parameters = new()
{
    MaxTokens = 1024,
    System = "你是知识百科全书",
    Messages =
    [
        new()
        {
            Role = Role.User,
            Content = "太阳系有多少颗行星?"
        },
    ],
    Model = "qwen/qwen3.5-9b",
};


var response = await client.Messages.Create(parameters);

foreach (ContentBlock block in response.Content)
{
    if (block.TryPickText(out TextBlock? text))
    {
        Console.WriteLine(text.Text);
    }
}

实际请求内容:

{
	"max_tokens": 1024,
	"system": "你是知识百科全书",
	"messages": [{
		"role": "user",
		"content": "太阳系有多少颗行星?"
	}],
	"model": "qwen/qwen3.5-9b"
}

响应结果:

{
  "id": "msg_y1q7enj0g9lurvq8yn676m",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "截至目前,国际天文学联合会(IAU)官方认定的太阳系行星数量为 **8 颗**。\n\n这八大行星按照距离太阳由近及远的顺序,依次是:\n\n1.  **水星** (Mercury)\n2.  **金星** (Venus)\n3.  **地球** (Earth)\n4.  **火星** (Mars)\n5.  **木星** (Jupiter)\n6.  **土星** (Saturn)\n7.  **天王星** (Uranus)\n8.  **海王星** (Neptune)\n\n**补充说明:**\n以前太阳系被认为有 9 颗行星,第 9 颗是“冥王星”。但在 2006 年,由于冥王星未能完全符合行星定义的三个标准(特别是它未能“清除其轨道附近的其他物体”),国际天文学联合会将其重新分类为“矮行星”,从而使得太阳系的正式行星数量定格为 8 颗。"
    }
  ],
  "model": "qwen/qwen3.5-9b",
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 25,
    "output_tokens": 212,
    "cache_read_input_tokens": 0
  }
}

返回的 Message 几个常用属性:

  • ContentIReadOnlyList<ContentBlock>,模型这一轮生成的所有内容块。正文文本在 TextBlock 里,要靠 TryPickText 取。
  • StopReasonApiEnum<string, StopReason>?,结束原因,取值 EndTurnend_turn,正常说完)、ToolUsetool_use,要求调用工具)、MaxTokensmax_tokens,达到 token 上限)、StopSequencestop_sequence,命中停止串)、PauseTurnpause_turn,暂停)、Refusalrefusal,拒绝回答)。读它的值用 message.StopReason?.Value
  • Usage:token 用量。InputTokens / OutputTokens 是非空 long,此外还有 CacheCreationInputTokens / CacheReadInputTokens(提示缓存相关)。
  • ID:这条消息的 id(如 msg_01...)。
  • Role / Type:固定为 "assistant" / "message"


读取正文文本:

foreach (ContentBlock block in response.Content)
{
    if (block.TryPickText(out TextBlock? text))
    {
        Console.WriteLine(text.Text);
    }
}

// 或者一次性把所有文本块拼起来
string answer = string.Join("",
    response.Content
        .Select(b => b.Value)
        .OfType<TextBlock>()
        .Select(t => t.Text));
Console.WriteLine(answer);

多轮对话

Anthropic 的 Messages 接口是无状态的(这点跟 OpenAI Responses 的 previousResponseId 不同),每一轮都要把完整历史自己拼好再发上去。做法跟 OpenAI Chat Completions 一样,把上一轮模型的 Content 整体作为一条 assistant 消息加回 messages,再追加新的 user 消息。

List<MessageParam> messages =
[
    new() { Role = Role.User, Content = "1+1=?" },
];

MessageCreateParams parameters = new()
{
    MaxTokens = 1024,
    Model = "qwen/qwen3.5-9b",
    Messages = messages,
};

// 第一轮
Message first = await client.Messages.Create(parameters);
if (first.Content[0].TryPickText(out TextBlock? firstText))
{
    Console.WriteLine($"[ASSISTANT]: {firstText.Text}");
}

messages.Add(new MessageParam
{
    Role = Role.Assistant,
    Content = new MessageParamContent(
        JsonSerializer.SerializeToElement(first.Content.Select(b => b.Json).ToArray())),
});

// 第二轮
messages.Add(new() { Role = Role.User, Content = "再加上 2 呢" });

parameters = new()
{
    MaxTokens = 1024,
    Model = "qwen/qwen3.5-9b",
    Messages = messages,
};

Message second = await client.Messages.Create(parameters);

if (second.Content[0].TryPickText(out TextBlock? secondText))
{
    Console.WriteLine($"[ASSISTANT]: {secondText.Text}");
}

第二轮实际请求体:

{
	"max_tokens": 1024,
	"model": "qwen/qwen3.5-9b",
	"messages": [{
		"role": "user",
		"content": "1+1=?"
	}, {
		"role": "assistant",
		"content": [{
			"type": "text",
			"text": "1 + 1 = **2**"
		}]
	}, {
		"role": "user",
		"content": "再加上 2 呢"
	}]
}

流式对话

流式是 Claude 接口的日常用法。关键区别前面说过:不是设 stream:true,而是调另一个方法 client.Messages.CreateStreaming(...),它返回 IAsyncEnumerable<RawMessageStreamEvent>,用 await foreach 逐条消费。

MessageCreateParams parameters = new()
{
    MaxTokens = 2048,
    Model = "qwen/qwen3.5-9b",
    Messages =
    [
        new() { Role = Role.User, Content = "讲一个关于写狼外婆的小故事" },
    ],
};

IAsyncEnumerable<RawMessageStreamEvent> updates =
    client.Messages.CreateStreaming(parameters);

Console.Write("[助手] ");
await foreach (RawMessageStreamEvent rawEvent in updates)
{
    if (rawEvent.TryPickContentBlockDelta(out var delta) && delta.Delta.TryPickText(out var text))
    {
        Console.Write(text.Text);
    }
}

实际请求:

{
	"max_tokens": 2048,
	"model": "qwen/qwen3.5-9b",
	"messages": [{
		"role": "user",
		"content": "讲一个关于写狼外婆的小故事"
	}],
	"stream": true
}

Anthropic 的 SSE 事件是按事件类型分发的:每条流式消息对应 RawMessageStreamEvent 的一个变体,TryPick 一次就知道它代表什么。常用的事件类型:

事件类型TryPick 方法作用
message_startTryPickStart流开始,.Message 是初始的 Message(含响应 id、Usage 的输入 token 等,此时 StopReason 还为空)
content_block_startTryPickContentBlockStart一个新的内容块开始(正文文本块、思考块、工具调用块等),.Index 是块下标
content_block_deltaTryPickContentBlockDelta增量就在这里。.Delta 又是一个联合,按 TryPickText / TryPickThinking / TryPickInputJson 等区分
content_block_stopTryPickContentBlockStop一个内容块结束
message_deltaTryPickDelta整条消息级别的更新,.DeltaStopReason.Usage 带输出 token 累计
message_stopTryPickStop整个响应结束

也就是说,「逐字输出」在 content_block_delta 里,而且增量本身还要再 TryPick 一次。

正文文本和深度思考分别走两条路径:

await foreach (RawMessageStreamEvent rawEvent in updates)
{
    if (rawEvent.TryPickContentBlockDelta(out var delta))
    {
        if (delta.Delta.TryPickThinking(out var thinkingDelta))
        {
            // 深度思考增量
            Console.Write(thinkingDelta.Thinking);
        }
        else if (delta.Delta.TryPickText(out var textDelta))
        {
            // 正文增量
            Console.Write(textDelta.Text);
        }
    }
}

跟 Chat Completions 手写 JSON 去捞 reasoning_content对比一下,Anthropic 这里是 SDK 原生支持的,扩展思考是一等公民,不需要自己解析原始 JSON。

如果你想一边流式输出、一边最后拿到一个完整的 Message,SDK 在 Anthropic.Helpers 里提供了 Aggregate() 扩展方法,或者用 Anthropic.Services.Messages.MessageContentAggregator 边收边聚合。


扩展思考

Claude 支持把深度思考过程暴露出来,模型在作答前先输出一段思考,再输出正文。开启方式是设置 Thinking

MessageCreateParams parameters = new()
{
    MaxTokens = 2048,
    Model = "qwen/qwen3.5-9b",
    Thinking = new ThinkingConfigEnabled() { BudgetTokens = 1024 },
    Messages =
    [
        new() { Role = Role.User, Content = "证明根号 2 是无理数" },
    ],
};

Message response = await client.Messages.Create(parameters);

foreach (ContentBlock block in response.Content)
{
    if (block.TryPickThinking(out ThinkingBlock? thinking))
    {
        Console.WriteLine($"[思考] {thinking.Thinking}");
    }
    else if (block.TryPickText(out TextBlock? text))
    {
        Console.WriteLine($"[正文] {text.Text}");
    }
}

实际请求:

{
	"max_tokens": 2048,
	"model": "qwen/qwen3.5-9b",
	"thinking": {
		"type": "enabled",
		"budget_tokens": 1024
	},
	"messages": [{
		"role": "user",
		"content": "证明根号 2 是无理数"
	}]
}

几个要点:

  • ThinkingConfigEnabled.BudgetTokens 是思考的 token 预算,必须 ≥ 1024 且小于 MaxTokens,否则请求会被拒。
  • 非流式响应里,Content 数组会先出现 ThinkingBlock(带 Thinking 文本和一个用于多轮续接的 Signature),再出现 TextBlock
  • 流式时,思考增量走 delta.Delta.TryPickThinking,正文增量走 TryPickText,前面流式那段已经演示过。
  • 如果思考内容被安全过滤,会出现 RedactedThinkingBlockTryPickRedactedThinking),里面不暴露原文。
  • 多轮对话里若要保留思考上下文,回填 assistant 消息时要带上 ThinkingBlock(直接用前面 JSON 往返的回填方式即可,整个 Content 原样塞回去最省事)。

提交工具与调用

我们沿用 OpenAI 那篇的灯控例子。先定义本地函数:

static Dictionary<int, bool> Lights = new()
{
    { 1, false }, { 2, false }, { 3, false }
};

static IReadOnlyDictionary<int, bool> GetLightState() => Lights;

static IReadOnlyDictionary<int, bool> OpenOrCloseLight(int index, bool state)
{
    Lights[index] = state;
    return GetLightState();
}

定义工具用 Tool + InputSchema。注意 Anthropic 的 schema 是直接平铺的(name/description/input_schema),没有 OpenAI 那层 function 嵌套;InputSchema.Properties 是个 Dictionary<string, JsonElement>,每个属性的 schema 用 JsonSerializer.SerializeToElement 构造:

using System.Text.Json;

Tool getLightState = new()
{
    Name = "GetLightState",
    Description = "获取所有灯的状态",
    InputSchema = new InputSchema(),
};

Tool openOrCloseLight = new()
{
    Name = "OpenOrCloseLight",
    Description = "打开或关闭灯",
    InputSchema = new InputSchema
    {
        Properties = new Dictionary<string, JsonElement>
        {
            ["index"] = JsonSerializer.SerializeToElement(
                new { type = "integer", description = "light index" }),
            ["state"] = JsonSerializer.SerializeToElement(
                new { type = "boolean", description = "open or close light" }),
        },
        Required = ["index", "state"],
    },
};

完整的工具调用循环:

static async Task Main()
{
	Tool getLightState = new()
	{
		Name = "GetLightState",
		Description = "获取所有灯的状态",
		InputSchema = new InputSchema(),
	};

	Tool openOrCloseLight = new()
	{
		Name = "OpenOrCloseLight",
		Description = "打开或关闭灯",
		InputSchema = new InputSchema
		{
			Properties = new Dictionary<string, JsonElement>
			{
				["index"] = JsonSerializer.SerializeToElement(
					new { type = "integer", description = "light index" }),
				["state"] = JsonSerializer.SerializeToElement(
					new { type = "boolean", description = "open or close light" }),
			},
			Required = ["index", "state"],
		},
	};

	AnthropicClient client = new(new ClientOptions
	{
		ApiKey = "sk-ant-xxx",
		BaseUrl = "http://127.0.0.1:1234",
		HttpClient = new HttpClient(new LoggingHandler()),
	});

	List<MessageParam> messages =
		[
		new() { Role = Role.User, Content = "获取所有灯的状态,并把 1、3 号的灯打开" }
		];


	bool requiresAction;
	do
	{
		requiresAction = false;
		MessageCreateParams parameters = new()
		{
			MaxTokens = 2048,
			Model = "qwen/qwen3.5-9b",
			Thinking = new ThinkingConfigEnabled() { BudgetTokens = 1024 },
			Messages = messages,
			Tools = [getLightState, openOrCloseLight],
		};

		Message response = await client.Messages.Create(parameters);

		// 1) 把这一轮 assistant 的完整输出(含 tool_use 块)原样回填进历史
		messages.Add(new MessageParam
		{
			Role = Role.Assistant,
			Content = new MessageParamContent(
				JsonSerializer.SerializeToElement(response.Content.Select(b => b.Json).ToArray())),
		});

		// 2) 扫出所有 tool_use 块,本地执行,收集成 tool_result
		var toolResults = new List<ToolResultBlockParam>();
		foreach (ContentBlock block in response.Content)
		{
			if (!block.TryPickToolUse(out ToolUseBlock? toolUse))
			{
				continue;
			}

			switch (toolUse.Name)
			{
				case nameof(GetLightState):
					toolResults.Add(new ToolResultBlockParam(toolUse.ID)
					{
						Content = JsonSerializer.Serialize(GetLightState()),
					});
					break;

				case nameof(OpenOrCloseLight):
					int index = toolUse.Input["index"].GetInt32();
					bool state = toolUse.Input["state"].GetBoolean();
					toolResults.Add(new ToolResultBlockParam(toolUse.ID)
					{
						Content = JsonSerializer.Serialize(OpenOrCloseLight(index, state)),
					});
					break;

				default:
					throw new NotImplementedException(toolUse.Name);
			}
		}

		// 3) 只要有工具被调用,就把结果作为一条 user 消息回填,再循环
		if (toolResults.Count > 0)
		{
			messages.Add(new MessageParam
			{
				Role = Role.User,
				Content = new MessageParamContent(toolResults
					.Select(r => (ContentBlockParam)r).ToList()),
			});
			requiresAction = true;
		}
	} while (requiresAction);

	// 最后打印 assistant 的正文
	foreach (MessageParam m in messages)
	{
		Console.WriteLine($"[{m.Role}] {m.Content}");
	}
}

实际请求:

{
	"max_tokens": 2048,
	"model": "qwen/qwen3.5-9b",
	"thinking": {
		"type": "enabled",
		"budget_tokens": 1024
	},
	"messages": [{
		"role": "user",
		"content": "获取所有灯的状态,并把 1、3 号的灯打开"
	}],
	"tools": [{
		"name": "GetLightState",
		"description": "获取所有灯的状态",
		"input_schema": {
			"type": "object"
		}
	}, {
		"name": "OpenOrCloseLight",
		"description": "打开或关闭灯",
		"input_schema": {
			"type": "object",
			"properties": {
				"index": {
					"type": "integer",
					"description": "light index"
				},
				"state": {
					"type": "boolean",
					"description": "open or close light"
				}
			},
			"required": ["index", "state"]
		}
	}]
}

第一轮响应结果:

{
  "id": "msg_1eq59rgu4hmkuywxccmspi",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "thinking",
      "thinking": "用户想要:\n1. 获取所有灯的状态\n2. 把第1号和第3号灯打开\n\n我需要先调用GetLightState来获取所有灯的状态,然后调用OpenOrCloseLight两次,分别打开第1和第3号灯(state=true表示打开)。\n\n让我按顺序执行这些操作。\n"
    },
    {
      "type": "tool_use",
      "id": "YH9doFoUfvWaonXqLwRwrV2IR2dLO8ZG",
      "name": "GetLightState",
      "input": {}
    }
  ],
  "model": "qwen/qwen3.5-9b",
  "stop_reason": "tool_use",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 352,
    "output_tokens": 85,
    "cache_read_input_tokens": 348
  }
}

模型要调用工具时,响应的 StopReasonToolUseContent 里会有一条 tool_use 块:

{
    "type": "tool_use",
    "id": "YH9doFoUfvWaonXqLwRwrV2IR2dLO8ZG",
    "name": "GetLightState"
}

你执行完,构造一条 user 消息回填,里面的 tool_result 块靠 tool_use_id 跟上面配对:

{
	"messages": [
        ...
        {
		"role": "user",
		"content": [{
			"type": "tool_result",
			"tool_use_id": "YH9doFoUfvWaonXqLwRwrV2IR2dLO8ZG",
			"content": "{"1":false,"2":false,"3":false}"
		}]
	}],
	"tools": [{
		"name": "GetLightState",
		"description": "获取所有灯的状态",
		"input_schema": {
			"type": "object"
		}
	}, {
		"name": "OpenOrCloseLight",
		"description": "打开或关闭灯",
		"input_schema": {
			"type": "object",
			"properties": {
				"index": {
					"type": "integer",
					"description": "light index"
				},
				"state": {
					"type": "boolean",
					"description": "open or close light"
				}
			},
			"required": ["index", "state"]
		}
	}]
}

上传图片

和 OpenAI 一样,附图的原理是把 user 消息的 content 从字符串换成内容块数组,混排文字和图片。Anthropic 支持两种图片来源:base64 内嵌(Base64ImageSource)和公开 URL(UrlImageSource)。

byte[] imageBytes = File.ReadAllBytes("34e7caa2-2852-458d-96cc-babff3c65c03.png");
string base64 = Convert.ToBase64String(imageBytes);

MessageCreateParams parameters = new()
{
    MaxTokens = 1024,
    Model = "qwen/qwen3.5-9b",
    Messages =
    [
        new MessageParam
        {
            Role = Role.User,
            Content = new List<ContentBlockParam>
            {
                new ImageBlockParam
                {
                    Source = new Base64ImageSource
                    {
                        Data = base64,
                        MediaType = MediaType.ImagePng,
                    },
                },
                new TextBlockParam("识别图片内容。"),
            },
        },
    ],
};

Message response = await client.Messages.Create(parameters);
if (response.Content[0].TryPickText(out TextBlock? text))
{
    Console.WriteLine($"[ASSISTANT]: {text.Text}");
}

实际请求体(图片以 image 块出现,source.typebase64,注意 Anthropic 的 data 是裸 base64,不带 data:image/png;base64, 前缀,这点跟 OpenAI 的 data: URI 不同):

{
	"max_tokens": 1024,
	"model": "qwen/qwen3.5-9b",
	"messages": [{
		"role": "user",
		"content": [{
			"type": "image",
			"source": {
				"type": "base64",
				"data": "iVBORw0KGg1qSkxS7L...",
				"media_type": "image/png"
			}
		}, {
			"type": "text",
			"text": "识别图片内容。"
		}]
	}]
}

注意:

  • MediaType 是枚举,可选 ImageJpeg / ImagePng / ImageGif / ImageWebP,要和真实图片格式一致。
  • base64 是裸数据。不要拼 data:image/png;base64, 前缀,那会当成 base64 内容的一部分导致解码失败。
  • 想用公开 URL,把 Source 换成 new UrlImageSource { Url = "https://..." } 即可(source.type 变成 "url")。
  • 跟 OpenAI 一样,混合内容块的顺序无所谓,但通常文字提示放图片后面,让模型先「看到」再回答。