资讯动态

Python调用通义千问API:从基础到函数调用与知识库实战

发布时间:2026/10/21 19:59:17 来源:尧图企业网站定制
我自己开始用Python调通义千问API那会儿其实挺狼狈的拿现成的代码改个Key跑通一次就以为完事了结果一到真实业务场景多轮对话记不住、历史一长就超限、并发一多就被限流、想让模型返回JSON结果回回在一堆解释文字里捞代码块。这篇就是我把这些坑挨个趟完之后的完整梳理——从最基础的鉴权和首次调用到多轮消息、流式输出、异步并发、JSON模式、函数调用最后用一个本地知识库问答Demo把前面所有能力串起来。内容不绕弯每段基本都带着能跑起来的代码和踩坑记录适合刚接触大模型API、想用Python快速接通的人也适合已经在接但想把深度能力用起来的工程师。1. 先想清楚你要把千问API当成聊天网页还是工程组件1.1 网页对话和API调用之间隔着什么网页版的千问是开箱即用的完整产品自动带记忆、有联网检索、有数据分析你只管打字。API恰恰相反它把模型推理能力原样交付给你但记忆、会话、调度、兜底全部要用你自己的代码实现。这是很多人第一次接API时最不习惯的一点你以为发一个请求过去第二次再发它还记得你。实际上API的每一次调用都是无状态的想让模型记得上一轮就必须把上一轮的对话记录完整放在请求里带回去。API不负责记住你是谁、聊过什么。所以我会把API理解成一个函数输入一组messages输出一个content。你的系统决定这个函数怎么被调用、中间状态怎么保存、结果怎么流转。把心态从用聊天工具切换成写程序调函数后面的所有设计都会顺很多。1.2 千问API的两条接入路线目前接千问API主要有两条路我两条都试过。第一条是百炼平台的原生SDKPython里装一个dashscope包调用非常简洁import dashscope from dashscope import Generation resp Generation.call( modelqwen-plus, prompt你好, ) print(resp)第二条是OpenAI兼容模式把base_url指到百炼平台的兼容入口然后就能直接用openai这个Python包发请求。这种模式下你手里已有的、基于openai包写的项目改改配置就能切过来。我的建议很明确新项目一律走OpenAI兼容模式。原因主要是生态。开源社区的各种日志、监控、链式调用框架默认都在跟openai包对接接入这个包能少写很多适配代码。原生SDK当然没问题但它更像官方给的快捷入口而不是通用生态接口。1.3 模型选型不是越贵越好千问系列在API侧有多个档位的模型技术选型时首先要决策的就是用哪一档。模型档位典型定位适合场景qwen-turbo快速、便宜测试、格式化、简单分类、大量低难度调用qwen-plus均衡、通用大多数生产环境的中等复杂度任务qwen-max强推理、较贵复杂分析、长文总结、代码规划qwen-long超长文本文档问答、合同解析等长上下文场景我自己踩过的坑是一开始为了效果无脑上qwen-max后来发现很多任务用qwen-plus就能拿同等质量成本和延迟却差了几倍。后来我定了个习惯每个功能先拿qwen-turbo跑通逻辑再按任务难度上调档位。批量任务里对不同子任务分派不同档位模型整体费用能降不少。这里引出一个核心观点API调用不是一个模型打天下而是按场景分派。先想清楚要做什么再决定调哪个模型比纠结某一行代码怎么写更重要。2. 五步跑通第一个请求密钥、环境、SDK与最小代码2.1 获取API Key控制台里最容易被忽略的细节接通前提是在阿里云百炼平台完成服务开通然后创建一个API Key。流程不算复杂登录控制台、开通模型服务、创建API-KEY、复制保存。细节在这里创建Key时平台会完整展示一次之后就只显示掩码丢了只能重新生成。我建议创建后立刻放到本机环境变量或团队的密钥系统里。不要把Key写死在代码里更不要提交到Git仓库。这是API开发的常识但我在真实项目里确实见过两次Key被提交进公共仓库的案例每次处理起来都非常被动。对本机开发最省事的做法是用环境变量import os api_key os.getenv(DASHSCOPE_API_KEY) if not api_key: raise ValueError(请先配置 DASHSCOPE_API_KEY 环境变量)2.2 安装依赖一个包就能跑OpenAI兼容模式下你只需要装openai这个Python包。如果之后还想用百炼的原生接口再补一个dashscope。pip install openai dashscope2.3 最小调用代码第一次拿到模型回复下面这段是整个调用的骨架建议先亲手跑一遍import os from openai import OpenAI client OpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1, ) response client.chat.completions.create( modelqwen-plus, messages[ {role: system, content: 你是一个可靠的编程助手回答尽量简洁。}, {role: user, content: 用Python写一个列表去重并保持顺序的函数}, ], temperature0.7, ) print(response.choices[0].message.content)运行后你会发现返回的是一个结构体真正想要的文字在response.choices[0].message.content里。很多人第一次接API就卡在这一步IDE里明明有类库提示就是不告诉你最顶层还有choices数组。原因是这类API天然支持一个请求返回多个候选结果虽然通常只取第一个但数据结构上保留了数组形态。返回体里另外两个值得关注的字段是finish_reason和usage。前者告诉你生成怎么结束的自然结束是stop因为长度限制被截断是length后者记录本轮输入输出消耗的token数后面算费用、定位问题都靠它。2.4 base_url拼写问题第一个经典坑兼容模式的base_url必须以/v1结尾。我第一次接的时候少写了最后的v1请求直接404。当时花了不少时间排查Key、排查权限最后发现就是URL少了一段。补充一点OpenAI SDK内部会自动处理认证头不需要手动加Bearer前缀。只有你用requests自己拼HTTP请求时才需要。通过这个最小骨架你已经掌握了一次完整请求的所有环节。接下来解决更关键的问题怎么让对话真正记住上下文。3. 多轮对话消息数组、历史管理与上下文窗口3.1 messages数组对话真正的状态入口API的每次调用都是无状态的但模型天然理解对话的语义你传进去的messages数组就是一段完整对话的模拟。数组里的消息有三种基本角色system设定模型行为的总纲放系统提示词user用户输入assistant模型之前的回复。多轮场景下必须把assistant的历史回复一起传回去否则模型不知道自己刚才说过什么。举个例子用户先问什么是闭包你得到一次assistant回复。下一条问它有什么用需要这样组装消息messages [ {role: system, content: 你是编程导师解释问题用通俗类比。}, {role: user, content: 什么是闭包}, {role: assistant, content: 闭包是函数记住并访问其定义时作用域内变量的能力可以理解成函数自带一个小型记忆包。}, {role: user, content: 它有什么用}, ]如果你把assistant那段删掉模型只能靠猜刚才聊了什么回答很可能会偏。所以多轮的正确姿势是每轮用户提问收到assistant回复后把这一对问答追加进本地会话列表下次请求整体作为messages传入。3.2 上下文窗口塞不下的时候怎么办每个模型都有上下文窗口的硬上限虽然qwen系列目前窗口都很大但总有边界。如果把聊天记录无限堆进去要么超出上限直接报错要么因为超长导致费用飙升、响应变慢。通用做法是在程序侧维护一个历史消息队列。我常用的策略不复杂永远保留system消息从最近的消息往前保留N轮比如20轮如果累计文本长度超过阈值比如模型窗口的一半继续丢最老的消息。一个朴素的按字符截断实现def trim_messages(messages, max_chars12000): kept [messages[0]] # system for msg in reversed(messages[1:]): if sum(len(m[content]) for m in kept) len(msg[content]) max_chars: break kept.append(msg) return kept[::-1]这个方案不精确计算token但对大多数场景够用。重点是要立一个规矩别让会话历史无限增长。一旦接入生产用户对话量大之后历史管理直接决定你的成本和出错率。3.3 采样参数temperature和top_p别一起乱调temperature控制输出随机性数值越大越发散越接近0越确定top_p是核采样阈值。平台建议这两个参数只调其中一个不要同时大幅调节否则输出会忽飘忽稳。我自己常用的经验值代码生成、JSON输出、数据提取temperature0.1~0.2通用问答temperature0.5~0.7创意写作、头脑风暴temperature0.8~0.9。另一个容易漏的是max_tokens。它限制的是本次生成的最大token数不是整个对话。如果设太小长回答会被截断finish_reason变length。我在早期开发时根本没设结果模型偶尔吐出特别长的内容费用明显上升。后来所有生成类调用都会显式设一个合理的max_tokens。4. 流式输出与异步并发别让用户干等十秒4.1 流式不只是体验好看默认非流式调用要等模型把整个响应全部生成完才一次性返回。一个复杂问题在qwen-max上可能生成几十秒用户端看到的就是一直转圈体验很差。流式输出streamTrue让模型每生成一小段就推给你。最直接的好处是首字延迟大幅降低用户通常在几百毫秒到一两秒内就能看到第一个字。流式在工程上还有一个容易被忽略的价值你可以在token到达时做中间处理。比如逐段渲染到界面、逐段写入缓冲区如果应用层做了敏感词过滤还能在生成中提前处理不用等整个回复完成再判断。4.2 streamTrue同步流式一段能直接用的代码兼容模式下开启流式几乎零成本def chat_stream(messages): response client.chat.completions.create( modelqwen-plus, messagesmessages, streamTrue, ) for chunk in response: delta chunk.choices[0].delta if delta and delta.content: print(delta.content, end)注意每个chunk里delta.content可能是空字符串比如首条chunk通常只有角色信息没有内容所以要判空。我确实见过有人没判断直接拼接结果输出总是多出几个None。还要留意流式响应中的usage字段有些兼容模式下流式结束时最后一块chunk会带上累计的usage统计。做计费统计时捕获这个特殊chunk比在每个chunk里瞎猜要准确得多。4.3 异步并发一批文件要总结时怎么办当你需要批量处理文本比如给一批文档生成摘要、批量生成标签同步逐条调用太慢了。换成AsyncOpenAI配asyncio.gather速度能拉起来import asyncio import os from openai import AsyncOpenAI aclient AsyncOpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1, ) async def summarize(text: str) - str: resp await aclient.chat.completions.create( modelqwen-turbo, messages[{role: user, content: f用一句话概括{text}}], temperature0.2, ) return resp.choices[0].message.content async def run(texts): sem asyncio.Semaphore(5) async def worker(t): async with sem: return await summarize(t) return await asyncio.gather(*[worker(t) for t in texts])Semaphore(5)表示同时最多5个请求在飞。这是客户端自我保护因为大模型API有并发上限盲目把并发拉到几十上百很快会撞见429限流。异步配上流式也可以同时用。把AsyncOpenAI的请求也加streamTrue不接收完整response改成async for chunk in response逐个处理。这套组合拳在批量生成和对话类产品里都很实用。5. 结构化输出让模型直接返回可以入库的JSON5.1 为什么需要JSON模式大模型默认返回自然语言。但如果要把它接进业务系统——比如解析出库单、生成配置条目、清洗用户提交的表单——自然语言就是灾难。你既没法直接入库也没法稳定映射字段。很多人最初的方案是让模型返回JSON然后用正则去提取。问题在于模型会在JSON外面包Markdown代码块围栏有时还夹解释性文字正则在不可控输出前面非常脆弱。所以平台直接提供了结构化输出能力让模型输出的格式能被程序稳定解析。5.2 response_format一行开启JSON模式兼容模式下请求参数里加一个response_format即可resp client.chat.completions.create( modelqwen-plus, messages[ {role: system, content: 请从用户输入中提取联系人信息只输出JSON。}, {role: user, content: 联系人A电话13800000000公司为某科技公司职位是后端工程师}, ], response_format{type: json_object}, ) content resp.choices[0].message.content print(content)这里有个特别容易踩的坑开启JSON模式后system或user消息里必须出现json这个词。我第一次加了这个参数直接报错查文档才知道这是模式切换的触发条件。无论如何让模型明确知道你返回的是JSON而不是让它自己猜。5.3 pydantic校验与异常捕获让返回数据真正可信JSON模式只保证格式是JSON不保证内容一定符合你的Schema。字段缺失、类型不对、名称混在一起都可能发生。所以我的生产代码里会再加一层校验from pydantic import BaseModel import json class Contact(BaseModel): name: str phone: str company: str title: str def parse_contact(content: str, finish_reason: str) - Contact: if finish_reason length: raise ValueError(输出被截断JSON可能不完整) data json.loads(content) return Contact(**data)finish_reason为length这个判断尤其重要一旦生成被max_tokens截断返回的字符串就是一段不完整的JSONjson.loads直接抛异常。正确做法不是让整个调用崩掉而是捕获异常后缩小任务范围或调大max_tokens重试一次。结构化输出带来的一个衍生价值是前端渲染时可以直接绑定字段不用再写一堆自然语言解析器。很多内部工具之所以能用API快速搭起来靠的就是这个参数。6. 函数调用把AI从会说话变成能办事6.1 函数调用到底是什么模型本质是概率生成器它没法执行真实代码、访问真实服务。函数调用的巧妙之处在于模型不亲自执行它只是判断当前用户需求需要调用哪个工具、参数是什么然后把一个结构化的工具调用请求返回给你。真正执行工具的是你的程序。你收到模型返回的tool_calls解析函数名和参数去执行真实函数再把执行结果作为一条tool消息回传给模型模型基于结果组织最终回答。这个链路虽然绕但解决的是让模型触达真实世界的问题。查天气、订会议室、查数据库都需要这种方式。6.2 一个最小可运行的工具调用Demo给对话助手加一个查天气的能力先定义工具描述tools [ { type: function, function: { name: get_weather, description: 查询指定城市的当前天气。当用户询问天气、温度、是否需要带伞时调用。, parameters: { type: object, properties: { city: {type: string, description: 城市名比如北京} }, required: [city] } } } ]调用分两步。先发带tools的请求resp client.chat.completions.create( modelqwen-plus, messages[ {role: system, content: 你是一个能查询天气的助手。}, {role: user, content: 北京今天需要带伞吗} ], toolstools, ) tool_calls resp.choices[0].message.tool_calls如果模型判断需要调用工具tool_calls里会有包含function.name和function.argumentsJSON字符串的列表。你的程序解析它执行真实函数def get_weather(city: str) - str: # 实际可接天气服务这里返回模拟结果 return 北京晴气温25度降水概率10% arguments json.loads(tool_calls[0].function.arguments) result get_weather(arguments[city])然后把这个结果作为tool角色消息回传让模型组织最终回答messages.append(resp.choices[0].message) messages.append({ role: tool, content: result, tool_call_id: tool_calls[0].id, }) final_resp client.chat.completions.create( modelqwen-plus, messagesmessages, toolstools, ) print(final_resp.choices[0].message.content)注意tool_call_id必须对得上否则模型不知道这条tool结果属于哪个调用。这个小字段是我见过出错最多的地方。6.3 工具设计的三个实战心得第一description要写得具体告诉模型什么时候用、什么时候不用。模型选工具靠语义匹配描述含糊就会选错。第二工具数量别贪多。我试过一次性给十个工具模型偶尔会传错参数类型或挑一个边缘工具。单场景保持2到4个工具选对率高很多。第三做好fallback。模型不是每次都调用工具它可能在tool_calls为空时直接说不知道。程序要对这种情况做兜底要么如实告诉用户结果要么换一个模型再试一次。函数调用是千问API深度应用里最实用的一环它让对话系统向自动化Agent迈进了一大步。7. 上线前避坑限流、超时、计费与内容安全7.1 HTTP错误码先看懂再重试真实环境下API请求不可能百分之百成功。常见错误排查顺序整理成一张表错误码含义常见原因处理方式401认证失败API Key错误或未生效检查环境变量和Key400请求参数错误messages结构不对、模型名错、JSON模式缺关键词按返回信息逐项排查404路径不存在base_url写错核对兼容模式地址429触发限流并发过高或频率超限退避重试、降低并发5xx服务端异常平台侧问题等几秒重试其中429是踩得最多的。应对429指数退避重试比固定间隔重试有效得多第一次等1秒第二次2秒第三次4秒直到上限。如果使用openai包它内部自带重试机制可以配置max_retriesclient OpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1, max_retries3, timeout30.0, )7.2 超时设置别让任务卡死在等响应大模型生成耗时不确定请求超时要放宽。我通常设置三档连接超时5秒整体请求超时30到60秒流式单块等待按秒级控制。批量任务中前面提过的信号量控制是必须的。没有并发控制时几十个请求同时涌出结果就是集体429反而比串行更慢。用异步或线程并行时默认连接池也可能不够用需要根据实际并发量适当调大。7.3 计费里最隐蔽的三个坑千问API按token计费输入和输出价格通常不同输出一般更贵。第一个坑是你误以为输入很便宜。在对话中每一轮请求都会把前面的历史再次送进去越长的会话输入token累计越多。用户每说一句你都在为之前所有内容再付一次费。对高频对话历史压缩是直接的成本优化点。第二个坑是流式输出照样按完整token计费。流式只是传输方式改变费用和完整返回完全一样。用流式是为了体验别指望它省钱。第三个坑是模型档位可能调整。平台调整模型版本时可能影响价格和效果生产环境要留意版本变化通知。必要时显式固定模型版本避免某天响应飘了才发现底层模型换了。7.4 内容安全与用户隐私大模型服务一般自带内容安全过滤。作为接入方你要知道两点一是过滤可能在返回侧表现为空内容或截断应用要处理这种模型没说话的情况二是别把不需要模型处理的数据送进去——姓名、电话、内部敏感字段先脱敏再进Prompt这是接入任何大模型API都该有的习惯。做企业级应用时最好在应用层加一道输出校验是否为空、是否明显偏离主题、是否包含危险指令。大模型擅长生成流畅但不一定正确的文字你的系统必须把它当成一个偶尔会撒谎的外部服务来对待。8. 实战拆解一个轻量本地知识库问答Demo8.1 目标与核心思路把前面所有能力串起来做一个能回答关于某份本地文档问题的工具。比如团队有一堆Markdown文档你想让同事直接向文档提问。最朴素的方案是把整篇文档全部塞进Prompt小文档还能凑合文档一大就废了既超上下文又浪费token。更稳的做法是检索增强先切分文档再用向量检索找到最相关片段只把片段拼进Prompt。完整链路是读取本地文档按段落切分用Embedding模型为段落生成向量用户提问后为问题生成向量做余弦相似度检索把命中的前几段文本加进messages作为补充材料调用千问API生成最终回答。8.2 可运行的简化代码百炼平台提供Embedding接口兼容模式下也能直接调用。一个极简实现如下import os import math import glob from openai import OpenAI client OpenAI( api_keyos.getenv(DASHSCOPE_API_KEY), base_urlhttps://dashscope.aliyuncs.com/compatible-mode/v1, ) def embed(text: str): resp client.embeddings.create( modeltext-embedding-v3, inputtext, ) return resp.data[0].embedding def chunk_doc(path, size400, overlap50): text open(path, encodingutf-8).read() chunks [] start 0 while start len(text): chunks.append(text[start:start size]) start size - overlap return chunks docs [] for path in glob.glob(knowledge/*.md): for i, c in enumerate(chunk_doc(path)): docs.append({doc: path, chunk: c, vec: embed(c)}) def cosine(a, b): dot sum(x * y for x, y in zip(a, b)) norm_a math.sqrt(sum(x * x for x in a)) norm_b math.sqrt(sum(y * y for y in b)) return dot / (norm_a * norm_b or 1) def retrieve(question, top_k3): qv embed(question) scored [] for item in docs: score cosine(qv, item[vec]) scored.append((score, item)) scored.sort(reverseTrue, keylambda x: x[0]) return [item for _, item in scored[:top_k]] def ask(question): hits retrieve(question) context \n\n.join(h[chunk] for h in hits) resp client.chat.completions.create( modelqwen-plus, messages[ {role: system, content: 只根据提供的资料回答问题资料不足时明确说不知道。}, {role: user, content: f资料\n{context}\n\n问题{question}} ], temperature0.2, ) return resp.choices[0].message.content这个实现处理几千行规模的文档完全够用。运行时主要成本来自Embedding建议把向量结果本地缓存起来不要每次启动都重新生成。8.3 效果局限与下一步实测下来这个Demo在十几个文档的小语料场景表现不错但短板也很清楚一是切分方式朴素段落边界可能把原本关联的内容切断二是没有重排机制检索前三段不一定最精准三是没有会话记忆每次提问都是独立的。如果往生产方向走三个升级路径比较有用换专业向量数据库支持更大语料和增量更新在检索后加重排环节让更相关的片段排到前面直接使用平台的RAG应用能力把文档上传、切分、检索、问答整体托管。最后分享一个我做这类工具的体会本地知识库问答真正拉开差距的不是模型选择而是检索质量。检索准了qwen-plus给出的回答基本可信检索不准换qwen-max也救不回来。所以调试时如果模型答不对第一件事是回头查检索回来的片段对不对而不是急着调Prompt或换更大的模型。先用小成本方案跑通再根据瓶颈决定升级哪一环这个顺序反过来多半会多花冤枉钱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑