Appearance
上一篇我们跑通了第一次模型调用。这一篇把这次调用彻底拆开:messages 到底是什么、怎么做流式输出(打字机效果)、怎么让模型记住多轮对话。还是老规矩,页面顶部可以切换 Python / Node.js。
messages:对话的最小单位
大模型的聊天接口不是「发一句、收一句」那么简单,它接收的是一个消息数组 messages,数组里每条消息都有一个 role(角色):
system:系统设定,给模型定人设、定规则,通常放在最前面,只写一次。user:用户说的话。assistant:模型之前的回复。多轮对话时,我们要把它历史回复也放回数组里,模型才「记得」。
关键点:接口本身是无状态的。模型不会自己记得上一轮,是我们每次把完整的对话历史重新发过去,它才能接上下文。理解这一点,后面的「记忆」就水到渠成了。
一、最基础的一次性调用
先封装一个最简单的调用,把复用的 client 单独拎出来。
python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
def ask(question: str) -> str:
resp = client.chat.completions.create(
model="qwen-plus",
messages=[
{"role": "system", "content": "你是一个简洁专业的助手。"},
{"role": "user", "content": question},
],
)
return resp.choices[0].message.content
print(ask("Python 里列表和元组的区别是什么?"))javascript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
});
async function ask(question) {
const resp = await client.chat.completions.create({
model: "qwen-plus",
messages: [
{ role: "system", content: "你是一个简洁专业的助手。" },
{ role: "user", content: question },
],
});
return resp.choices[0].message.content;
}
console.log(await ask("JavaScript 里 == 和 === 的区别是什么?"));temperature 是常用的可选参数:0 附近回答更确定、更稳定,适合分类抽取这类任务;调高(如 0.8)更发散,适合创意写作。想控制随机性时在 create 里加上它即可。
二、流式输出:打字机效果
一次性调用要等模型把整段话生成完才返回,长回答会让用户干等。流式输出则是模型每生成一小块(token)就推一块过来,前端立刻显示,体验上就是「一个字一个字往外蹦」。做法是把 stream 打开,然后遍历返回的数据块。
python
def ask_stream(question: str):
stream = client.chat.completions.create(
model="qwen-plus",
messages=[{"role": "user", "content": question}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True) # 实时打印,不换行
print()
ask_stream("用三句话介绍一下杭州。")javascript
async function askStream(question) {
const stream = await client.chat.completions.create({
model: "qwen-plus",
messages: [{ role: "user", content: question }],
stream: true,
});
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta); // 实时输出,不换行
}
process.stdout.write("\n");
}
await askStream("用三句话介绍一下杭州。");注意流式返回的每块里,内容在 delta.content 而不是 message.content,而且有些块(比如最后的结束块)内容为空,要判空后再拼接。后面做 Web 界面时,我们会把这个流经过 SSE 推给浏览器,实现网页上的打字机效果。
三、多轮对话:让模型「记住」
前面说过接口无状态,那要实现连续对话,就得自己维护一个 messages 列表:每轮把用户的话追加进去、调用模型、再把模型的回复也追加进去。下一轮把整个列表重新发过去,上下文就接上了。
python
history = [{"role": "system", "content": "你是一个耐心的编程老师。"}]
def chat(user_input: str) -> str:
history.append({"role": "user", "content": user_input})
resp = client.chat.completions.create(model="qwen-plus", messages=history)
answer = resp.choices[0].message.content
history.append({"role": "assistant", "content": answer}) # 把回复也存回历史
return answer
print(chat("我想学装饰器,先给我一句话概括。"))
print(chat("那它和闭包有什么关系?")) # 这里的「它」指装饰器,模型能接上javascript
const history = [{ role: "system", content: "你是一个耐心的编程老师。" }];
async function chat(userInput) {
history.push({ role: "user", content: userInput });
const resp = await client.chat.completions.create({
model: "qwen-plus",
messages: history,
});
const answer = resp.choices[0].message.content;
history.push({ role: "assistant", content: answer }); // 把回复也存回历史
return answer;
}
console.log(await chat("我想学闭包,先给我一句话概括。"));
console.log(await chat("那它和作用域有什么关系?")); // 「它」指闭包,模型能接上跑第二句时你会发现,模型准确理解了「它」指代前文,这就是多轮记忆生效了。
一个要注意的坑:历史会越滚越长,token 消耗和费用也随之上涨,太长还会超出模型上下文窗口。生产里通常会做「滑动窗口」(只保留最近 N 轮)或「摘要压缩」(把老对话总结成一小段)。这些「记忆管理」的话题,等我们进入 LangChain 篇会用更优雅的方式处理。
小结与预告
这一篇你掌握了大模型调用的三件套:一次性调用、流式输出、多轮对话,并且理解了最核心的一点——接口无状态,记忆靠我们自己维护 messages。
到这里你已经能用裸 SDK 写出一个像样的命令行聊天机器人了。但随着功能变复杂(模板、解析、检索、记忆、工具……),纯手写会越来越乱。下一篇《提示词工程》,我们先把「怎么把话对模型说清楚」这件事做扎实——包括结构化输出、让模型稳定吐 JSON,以及金融文本分类/抽取的实战案例。