最近在对接一个多模态识别的需求,需要把图片和文字放在同一个请求里发给模型,让它输出结构化结果。模型这边用的是 Qwen3.5-27B,联调工具我选了 Postman——倒不是因为它有什么 AI 魔法,而是它对图文请求构造、环境变量管理、批量测试和响应断言这套流程的支持确实够顺手。这篇文章就把从零开始用 Postman 配置图文 API 请求、再到响应断言验证的完整过程拆开讲一遍。适合已经拿到 API 访问权限、正准备上手联调的开发者,也适合刚接触多模态 API、想快速验证模型能力的朋友。里面提到的所有步骤都可以直接照着操作,相关参数和脚本我会一并给出。
1. 环境准备与请求前必读
1.1 本轮工具清单与版本注意事项
开始配置之前,先把工具链理清楚。Postman 我建议直接用最新版,如果你还在用老旧的 8.x 版本,功能上其实也够,但新版对请求体格式校验、断言脚本提示、集合 Runner 的稳定性都要好不少。我这边的环境是 macOS 上的 Postman v10 系列,Windows 上的布局基本一致,只是快捷键略有差别。
另外你要确认手头的模型服务端能力。Qwen3.5-27B 这种多模态模型,输入侧支持文本和图片两种模态,输出侧通常返回纯文本内容,也会附带 token 使用量、请求 ID 这类元信息。你拿到手的 API 文档大概率有两种图片传入方式:一是传公网图片 URL,二是传 base64 编码后的图片内容。这两种方式我在后文都会讲到,它们在 Postman 里的配置方式差异不小。
还有一个容易被忽略的点:接口的 Base URL 和鉴权字段。有些服务商的 endpoint 是统一的,有些则按模型区分,比如/compatible-mode/v1/chat/completions这种路径风格。先把接口文档完整读一遍,确认三件事——endpoint 地址、Header 里的鉴权 key 名称、POST 请求体的字段结构。这三个信息不确认,后面全是猜。
1.2 拿到密钥后的核对清单与初检
我习惯在接入任何大模型 API 时先做一次“健康检查”,不直接上 Postman,而是先在命令行里用最简结构验证连通性。这一步能帮你排除掉"到底是网络问题还是配置问题"这种最常见的内耗。比如先拿一条最简单的纯文本消息请求,确认能正常返回结果后,再往请求体里加图片字段。
你手头需要准备的东西并不复杂:
- 一个可用的 API Key(或者叫 Access Token);
- 目标模型的完整模型名(确认是写
qwen3.5-27b还是类似变体); - 图片素材:建议准备一张体积不大、但特征清晰的本地图,签收单、产品包装、截图都可以;
- 图片 URL:如果接口支持公网图片,准备一个可直连的图片地址,别用需要登录才能访问的内网地址。
拿到 API Key 之后第一件事,不是直接写 Postman,而是先想清楚怎么管理这个密钥。我的经验是别把硬编码直接贴在请求 Header 里测试,而是从第一步就建立环境变量。原因很简单:一旦你同时测试生产环境和测试环境,或者换了新 Key,硬编码会让你挨个请求找一遍。不规范,还容易泄密。
所以打开 Postman 后先进入 Environments(环境管理),新建一个环境,我通常起名dev-env。在这个环境里定义好两个变量:
| 变量名 | 初始值 | 当前值 |
|---|---|---|
base_url | 你的接口域名,比如https://api.example.com | 同上 |
api_key | 你的完整 API Key | 同上 |
变量建好之后,所有请求里的域名和密钥都改成写{{base_url}}和{{api_key}}的引用形式。这样后期换环境、换密钥,只动环境变量一个地方就够了,这也是 Postman 使用中算不上秘密但极其重要的习惯。如果连环境变量都还没用起来,强烈建议现在就改,不然后面的批量测试和团队协作会很难受。
2. Postman 请求配置全流程解析
2.1 新建请求与 URL 拼接方法
健康检查通过后,开始在 Postman 里新建请求。点击左上角的 New 菜单,或者直接用快捷键Ctrl+N / Cmd+N,选择 HTTP Request。请求名我建议直接用接口语义来命名,比如Qwen3.5-27B 图文识别,别用test这种明天就不知道是测什么的文件名。
URL 地址栏里填的是完整请求地址,结构是“基础域名 + 接口路径”。比如我这边测试用的地址是{{base_url}}/v1/chat/completions,其中{{base_url}}会自动被环境变量替换成真实域名。路径部分以你拿到的文档为准,不要照抄任何教程里的路径,因为不同部署方的路由设计差异很大。
请求方法设置为 POST。大模型对话类接口几乎都是 POST,因为你需要把大量的输入内容放在请求体里交给服务端。这一点上如果你之前习惯用 GET 传参,需要调整一下观念:GET 方式的 URL 长度限制根本无法承载图像 base64 数据,POST 的 body 才是放下图片的地方。
URL 拼好之后,先把请求体暂时留空,切到 Headers 页签确认默认请求头。Postman 会为 POST 请求自动加上Content-Type: application/json,这点通常没问题。重点需要手动加的是鉴权信息,也就是 API Key 怎么传给服务端。
2.2 Headers 鉴权与 Content-Type 的规范写法
鉴权 Header 常见的有两种风格:一种是服务商要求用Authorization: Bearer <token>,另一种是自定义 Header,比如X-API-Key: <your-key>。具体用哪种,以你拿到的接口文档为准,不要凭感觉猜。
接上面说的环境变量,Headers 里的写法应该是这样:
Authorization: Bearer {{api_key}}注意,如果接口要求的是自定义 Header,则在 Key 那一列填X-API-Key,Value 列填{{api_key}}。两个变量一模一样,区别只在于 Key 的名称。你可以在请求的 Headers 页签里看到这些键值对,也可以在最底部的"Code"按钮里查看请求生成的真实 cURL 语句,这是排查鉴权问题最快的方式之一。
还需要检查的一个点是 Host 头。如果你用了某些云厂商网关,可能还要求配置Host为特定的值。一般来说 Postman 会自动填充,但如果你遇到 403 或证书类报错,可以对照接口文档检查一下这个头。我在实际联调中踩过一次坑:域名走了网关转发,但 Host 头还是原始域名,导致鉴权签名一直校验不过。这类问题在 Postman 里很难看出来,因为 UI 上不会标红,必须打开生成的 cURL 代码对比检查才能发现。
Content-Type 通常是application/json,但如果你的图片采用 base64 方式放在 JSON 里,Content-Type 不需要改成multipart/form-data。这里很多新手会搞混,以为传图片就得用 form-data,事实上对于大模型 API,base64 图片已经被封装成 JSON 字符串的一部分,走一层 JSON 序列化就够了。如果你的服务商明确要求 multipart 混合表单,那另说,但绝大多数兼容 OpenAI 格式的接口都走纯 JSON。
2.3 请求体组织:文本与图片的两种传入方式
请求体是整个配置过程的核心。我以常见的 OpenAI 兼容消息结构作为示例,Qwen3.5-27B 的组织方式大体一致,细节以文档为准。结构上就是messages数组里放一条用户消息,用户消息的content字段不再是一段字符串,而是一个数组,里面的元素分别代表文本和图片。
先看图片 URL 方式的请求体:
{ "model": "qwen3.5-27b", "messages": [ { "role": "user", "content": [ { "type": "image_url", "image_url": { "url": "https://example.com/images/packaging.jpg" } }, { "type": "text", "text": "请描述这张产品包装图,重点关注文字内容和颜色搭配。" } ] } ], "max_tokens": 512, "temperature": 0.3 }注意几点:第一,image_url是一个对象,不是字符串,不要写成"image_url": "..."这种错误格式;第二,文本和图片没有先后顺序的硬性限制,但我通常把图片放前面、文本提示放后面,方便人类阅读;第三,max_tokens建议设成 300 到 600 之间,多模态识别场景的返回内容通常不会太长,设得太大浪费延迟,设太小会截断结果。
再看 base64 方式的请求体。这种方式需要先把图片转成 base64 字符串,然后拼到 JSON 里。Postman 本身不自带图片转 base64 的能力,我一般先在本地用命令行处理:
base64 -w 0 packaging.jpg > packaging_b64.txt生成的文本文件里那一长串字符就是 base64 编码结果。然后在 Postman 的请求体里,把image_url.url字段替换成`data:image/jpeg;base64,` + 那串字符,也就是最终长这样:
{ "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQ==..." } }这里有一个性能上的重要建议:Postman 编辑大段 base64 字符串非常卡,特别是几 MB 的图片编码后接近上百万字符,粘贴、修改、保存都会让人想砸电脑。我的做法是准备一张尽量小的测试图,长边控制在 1024 像素以内,JPG 格式,文件体积控制在 200KB 以下。小图不仅能避免卡顿,识别速度也更快。你真正要处理大图时,也不建议直接把超大 base64 塞进 Postman 做调试,先用脚本验证逻辑,再回到 Postman 做接口联调会更高效。
无论用哪种方式,请求体填完后,先不要急着点 Send。我习惯先看一眼请求体的 JSON 格式是否合法,Postman 编辑区会自动做语法高亮,如果某个括号或逗号出了问题,红色波浪线会提示你。也可以在请求体左上角的格式化按钮(Beautify)整理一遍。格式问题是最基础的检查项,做完这些,点 Send 看初步结果。
3. 响应断言与数据验证实战
3.1 先看懂响应结构再写断言
请求发送成功后,Postman 的响应区会显示返回结果。不同部署方返回的结构有差异,但大体会长成这样的风格:
{ "code": 0, "message": "OK", "data": { "text": "这是一张产品包装图,主色调为蓝色,盒面有白色文字...", "model": "qwen3.5-27b", "usage": { "prompt_tokens": 32, "completion_tokens": 45, "total_tokens": 77 } }, "request_id": "8ad3c72e2f1a4b3b8e9e0a2f0d6e6c31" }也可能是直接 Gemini 风格的candidates结构,或 ChatGLM 风格的自定义结构。你不需要把所有字段都记下来,只需要确认三件事:第一,哪个字段表示“本次请求成功”的判断标志;第二,哪个字段放的是真正的模型输出文本;第三,哪个字段是请求 ID 或用例 ID,方便后续排查问题。
在写断言之前,我强烈建议你先在响应区里手动点开返回的 JSON 树形结构,找到这几个关键字段的位置。不要照着别人的断言脚本瞎改字段路径,不同部署方可能把输出放在data.text、data.choices[0].message.content或者response里。路径不对就断言失败,甚至断言永远失败后你还以为是模型出错了。
如果你用的是 OpenAI 兼容格式,输出文本通常在choices[0].message.content下面。而如果是自定义封装结构,可能像上面那样包了一层data.text。所以我要再强调一次:字段路径以实际返回为准,这是断言环节最核心的工作,没有捷径。
3.2 基于 Postman 脚本的断言清单
Postman 的断言写在 Responses 区域下面的 Tests 页签里,本质是 JavaScript 脚本。我整理了一套在多模态图文接口项目中实际用过的核心断言组合,你可以在pm.test里根据自己的字段路径微调后直接使用。
第一类是检查 HTTP 状态码和基本成功标志:
pm.test("HTTP 状态码为 200", function () { pm.response.to.have.status(200); }); pm.test("接口返回成功标志", function () { const json = pm.response.json(); pm.expect(json.code).to.eql(0); });第二类是检查返回文本的内容类型,这一步特别关键,能帮助你确认模型确实读到了图片而非纯文本兜底:
pm.test("返回结果包含关键特征词", function () { const json = pm.response.json(); const text = json.data.text; pm.expect(text).to.include("包装"); pm.expect(text.length).to.be.above(20); }); pm.test("返回内容不包含报错信息", function () { const json = pm.response.json(); pm.expect(json.data.text).to.not.include("error"); });特征词检查需要你想清楚自己图片里的确定性信息。比如我测试用的包装图上面一定有“净含量”三个字,那就断言返回文本包含这个字段。如果你只是随手拿一张生活照,特征词无从谈起,那这条断言的意义就不太大了。
第三类是检查 token 使用量和请求 ID 是否正常返回:
pm.test("请求 ID 存在", function () { const json = pm.response.json(); pm.expect(json.request_id).to.be.a("string"); pm.expect(json.request_id.length).to.eql(32); }); pm.test("token 统计字段正确", function () { const json = pm.response.json(); pm.expect(json.data.usage.total_tokens).to.be.above(0); });这些断言脚本在发送请求后会自动执行,右侧会显示每一条是否通过。你不需要一次写完,可以先运行一遍请求,观察返回结构后,再逐步补充断言。断言的价值在于把你的“肉眼检查”变成可重复的“自动检查”,以后每次改参数重发都能快速知道结果是否异常。
3.3 用数据文件批量跑断言
单张图片测试通过后,你就可以进入批量验证阶段了——把多张图片和多条提示词组合成数据文件,让 Postman 集合 Runner 一次性跑完全部用例。这个功能对模型接口测试特别实用,因为它能在几分钟内验证模型在不同图片上的表现稳定性、响应延迟和是否偶发超时。
先准备好一个 CSV 或者 JSON 文件作为测试数据。JSON 格式比较灵活,我的做法是准备这样的数据文件:
[ { "image_path": "https://example.com/images/packaging_blue.jpg", "prompt": "描述这张包装图的配色和文字", "expect_keyword": "蓝色" }, { "image_path": "https://example.com/images/packaging_red.jpg", "prompt": "描述这张包装图的配色和文字", "expect_keyword": "红色" } ]然后在 Postman 请求体里使用变量占位:
{ "model": "qwen3.5-27b", "messages": [ { "role": "user", "content": [ { "type": "image_url", "image_url": { "url": "{{image_path}}" } }, { "type": "text", "text": "{{prompt}}" } ] } ] }断言里同样可以引用变量:
pm.test("识别结果包含预期特征词", function () { const json = pm.response.json(); pm.expect(json.data.text).to.include(pm.variables.get("expect_keyword")); });打开集合 Runner,选择这个请求和对应的 JSON 数据文件,Postman 会依次发起多次请求并统计通过率。我实测下来,一批十个用例跑完通常只需要几分钟,比一个一个人肉验证高效太多。如果某个用例断言失败,Runner 结果页会直接标红,点击就能看到响应内容,定位问题非常快。
批量跑完之后还有一个高级技巧:把响应时间也固化成断言。多模态接口的响应时间波动通常比纯文本接口大,一张大图动辄几秒很正常。你可以设置一条上限断言来防止服务端异常拖垮业务:
pm.test("响应时间低于10秒", function () { pm.expect(pm.response.responseTime).to.be.below(10000); });这条断言在单次调试时用处不大,但在批量跑集合并做性能摸底时就很有价值了。它能把一些偶发的慢请求直接标记出来,方便你逐个查看是否跟图片大小有关。
4. 常见问题排查与避坑记录
4.1 鉴权失败与超时场景处理
整个配置流程中,鉴权类问题是出现频率最高的,主要表现为 401 Unauthorized 和 403 Forbidden 两类情况。
401 的本质是服务端不认识你的身份,最常见的原因是 Header 格式写错。比如接口要求X-API-Key而你写成了Authorization,或者把 Key 多打了一个空格。排查方式是打开请求的 Code 按钮,查看生成的 cURL 原文,把每一行 Header 和文档示例逐字对比。我碰到过一个很隐蔽的问题:环境变量里的 Key 值尾部多了一个不可见的换行符,Postman 在 UI 上完全看不出来,但请求头里就携带了这个换行符,服务端解析时直接拒掉。后来我在环境变量编辑区把 Key 重新复制了一遍才解决。这个案例告诉我,环境变量的赋值最好用复制粘贴,并且填入后可以在终端里执行echo $KEY感知一下值有没有多余字符。
403 则通常是身份识别成功、但权限不足。可能原因包括:API Key 没有开通对应模型的访问权限;模型名称拼写错误导致被归为未知模型;或者当前账号的并发额度已经用完。403 一般不会出现在 Header 格式层面,更可能是账号权限问题,需要去服务商控制台检查。
超时问题同样常见。默认的 Postman 请求超时时间是 30 秒,多模态识别如果传大图或者服务端排队严重,很容易碰上限。你可以在请求设置(Settings)里把超时时间调到 60 秒或更长。但我更建议从源头优化:控制图片大小,把长边控制在 1024 以内,文件压缩到几百 KB,响应时间通常能降到 5 秒左右。如果连续出现响应时间超过 30 秒的情况,优先考虑是不是图片 base64 过大导致上传链路阻塞,而不是服务端算力问题。
4.2 图片内容出不来或返回 400 的请求体陷阱
我遇到过好多次请求发出去,服务端返回 400 Bad Request,响应体里报“Invalid input"或"malformed request”。这类问题的根源几乎都在请求体 JSON 结构上。
最常见的错误是把image_url写成了字符串,这是新手最容易踩的格式坑。规范的结构是嵌套对象:"image_url": {"url": "..."},注意url这个子字段名也不能改。如果你按自己的理解写成"image": {"src": "..."},服务端不认识,直接报错。
第二个重要陷阱是 base64 字符串里包含了换行。如果你在命令行用base64命令生成字符串时没有加-w 0,输出可能会被自动切成多行。把这些带换行的字符串直接贴进 JSON,JSON 解析器会认为字符串被提前截断,请求体直接非法。解决办法是生成时强制不换行,或者用一个简单的 Python 脚本处理:
import base64 with open("image.jpg", "rb") as f: data = f.read() b64_str = base64.b64encode(data).decode("utf-8") print(f"data:image/jpeg;base64,{b64_str}")第三个坑是图片 URL 无法直连。如果你传的是公网 URL,一定要确认服务端能访问到。我之前用过一个本地测试图片地址,Postman 这边访问没问题,但服务端的机房网络根本拿不到,返回的识别结果就是空白或者报错。验证方式很简单:把图片 URL 扔到浏览器无痕窗口里访问,能直接打开说明可能可用,打不开则一定不行。更保险的方式是直接转 base64 传。
还有一类情况是模型明明收到了图片,但返回的内容跟图片无关,看起来像是只处理了文本。这种问题通常是消息结构里图片格式不符合要求,或者模型本身不支持某个图片格式(比如 WebP 换成 PNG 就好了)。你可以尝试把图片转为 JPG 或 PNG 格式,并用长宽比正常的图片再做一次测试。
4.3 断言“永远成功”或“永远失败”的原因
断言脚本写完之后还有一个让人头疼的表现:无论请求结果如何,断言全都通过了,看起来非常完美,实际上半点作用没有。这种“假成功”通常是因为脚本拿的不是真实响应体,而是空对象。
比如你写pm.response.json(),但实际返回的内容不是合法 JSON,那这个调用可能抛异常导致脚本中断,而 Postman 的默认行为会把这轮测试视为失败。可有些情况下,你写的是:
pm.test("检查code", function () { const json = pm.response.json(); pm.expect(json.data.code).to.eql(0); });如果实际响应里没有data.code这个字段,pm.expect(undefined).to.eql(0)会失败,这倒还正常。真正的问题是另一种写法——你在响应还没返回时就对空对象做了断言,比如测试脚本中使用了错误字段路径但表达式恰好不会抛错,例如pm.expect(undefined).to.not.eql(null),这条断言就会“通过”,但毫无意义。
我建议写断言时遵循一个原则:第一层断言必须先验证响应存在且 JSON 能正常解析。等你确认了响应结构,再去顺着真实字段逐级展开。不要从一个教程里复制一大段断言脚本就完事了,至少把字段路径用实际响应核对一遍。
另外,断言失败时 Postman 默认不会阻止请求流程,它只是标红。如果你在集合 Runner 里批量跑测试,建议在失败情况下追加一条主动日志,方便事后定位:
pm.test("请求成功且包含输出", function () { const json = pm.response.json(); if (!json || !json.data || !json.data.text) { console.log("请求ID:", json.request_id, "原始响应:", JSON.stringify(json)); } pm.expect(json.data.text).to.be.a("string"); });通过这种方式,就算断言失败,你也能从控制台拿到原始响应和请求 ID,不需要再手动重发一次请求去复现问题。
最后再分享一个小经验:Qwen3.5-27B 这类多模态模型的接口调试,最难的部分往往不是某个请求怎么构造,而是如何在反复调整图片、提示词和参数的循环中保持验证方法的稳定。Postman 的环境变量、集合 Runner 和断言脚本,本质上是一套把“人工测试”变成“可复用验证流程”的机制。等你把这套流程跑熟了,再回到代码里去调用这个模型接口,很多坑都已经在前面踩完了,联调效率会快很多。