首先进行技术选型。
1 Deepseek文本大模型,其官方具备面向nodejs环境的openai(调用模型的sdk)。
2 环境直接起一个react项目,直接在前端项目中调用Deepseek(仅学习用,生产还是要起nodejs轻服务)。

一 流式多轮对话

1.1 基础请求

先发出一个简单的文本请求。
下面的apiKey需要替换为自己的key,在deepseek开发平台中获取。

下面的代码包含了OpenAI的简单用法,

import OpenAI from "openai";
import { useState,useEffect } from 'react';

const openai = new OpenAI({
    baseURL: 'https://api.deepseek.com',
    apiKey: 'xxxxxxxxxxxxxxxxxxxxx',
    dangerouslyAllowBrowser: true,
});

export default function DeepseekPage() {
    const [chatList, setChatList] = useState([])

    useEffect(() => { 
        openai.chat.completions.create({
            messages: [
                { role: "system", content: "You are a helpful assistant." },
                { role: "user", content: "hello." },
            ],
            model: "deepseek-v4-flash",
            thinking: {"type": "disabled"},
            stream: false,
        })
        .then(res => {
            console.log(res.choices[0].message.content);
        });
    }, [])

    return (
        <div>
            { chatList.map((msg, index) => (
                <div key={index}>
                    <strong>{msg.role}:</strong> {msg.content}
                </div>
            )) }
        </div>
    )
}

注意下面代码中打印了一个res.choices[0].message.content,内容如下:

{
  "role": "assistant",
  "content": "Hi! How can I help you today?"
}

其中content是内容,role是角色。
现在页面上已经展示了一行返回内容了。
在这里插入图片描述

1.2 多轮对话

多轮对话的核心是将第一轮中模型的输出添加到 messages 末尾。
上面代码中第一次请求的参数是这样的:

[
   { role: "system", content: "You are a helpful assistant." },
   { role: "user", content: "hello." },
]

我们将返回体和用户的写入内容追加到后面,使参数变为:

[
   { role: "system", content: "You are a helpful assistant." },
   { role: "user", content: "hello." },
   { role: "assistant",  content: "Hi! How can I help you today?"}{ role: "user", content: "xxxxxx" },
]

同时在用户点击发送后触发接口请求。
现在我们要做两件事:
1 上面代码中的useEffect要重复触发,触发条件是有新的用户信息产生时,所以书写一个变量记录该信息列表:chatList,并将其加入到useEffect依赖项中。
2 状态的触发需要入口,所以添加input和submit入口。

export default function DeepseekPage() {
    const [chatList, setChatList] = useState([
        { role: "system", content: "You are a helpful assistant." },
        { role: "user", content: "hello." },
    ])
    const [input, setInput] = useState('')

    useEffect(() => {
        if(chatList[chatList.length - 1].role !== 'user') return
        openai.chat.completions.create({
            messages: chatList,
            model: "deepseek-v4-flash",
            thinking: {"type": "disabled"},
            stream: false,
        })
        .then(res => {
            console.log(res.choices[0].message.content);
            setChatList([...chatList, res.choices[0].message])
        });
    }, [chatList])

    return (
        <div>
            { chatList.map((msg, index) => (
                <div key={index}>
                    <strong>{msg.role}:</strong> {msg.content}
                </div>
            )) }
            <Space vertical size="medium">
                <Space.Compact style={{ width: '100%' }}>
                <Input defaultValue="Combine input and button" value={input} onChange={(e)=>setInput(e.target.value)}/>
                <Button type="primary" onClick={() => {setChatList([...chatList, {role:'user', content: input}])}}>Submit</Button>
             </Space.Compact>
            </Space>
        </div>
    )
}

现在输入文字点击按钮,就可以实现多轮对话了。
在这里插入图片描述

1.3 流式传输

现在ai返回的语句是一次性全部返回的,等待时候较长。
我们使用ai对话的时候,可以让它陆续返回,实现打字机效果,就是流式传输了。
首先将stream设置为true,让其开启流式传输。
其次要在js中补充承接逻辑。

1.3.1 fetch

如果使用fetch来处理,返回值会返回两个重要的数据:value和doneReading。
doneReading是流式数据是否已全部返回。
value是返回的内容,使用new TextDecoder().decode(value)进行解析读取。
返回的内容解析后可能出现的情况示例如下:
第一轮返回:

data: {"choices":[{"delta":{"content":"你

第二轮返回:

"}}]}\n\n

可以看到解析的时候,一个有效json会分成多次返回,一个有效json由data开启,由\n结束。
整个代码逻辑如下:
1 判断是否已经全部返回),否则循环获取数据。
2 将本次数据与上次数据相结合,形成本次校验(是否形成有效json?)数据(chunkValue)。
3 将有效数据以’data: ‘为开头,’\n’为结尾取出
4 解析json串后赋值

1.3.2 openai

我们这里使用openai来处理。
首先将openai.chat.completions.create中的stream设置为true,让其开启流式传输。

先把useEffect中的代码提取出来,成为方法使用。因为这个方法是独立性比较强的,所以不需要封装为hook。

const getCharDate = async (dataList, callback, stream = false) => {
    let res = await openai.chat.completions.create({
        messages: dataList,
        model: "deepseek-v4-flash",
        thinking: {"type": "disabled"},
        stream: stream,
    })
    callback([...dataList, res.choices[0].message])
}

现在添加流式处理逻辑。
openai已对其做了封装,实现流式就比较容易。
其会返回异步迭代器(Async Iterator),直接使用 for await…of 循环来逐块获取数据即可。

for await (const chunk of res) {
    console.log(chunk)
}

上面打印出的chunk示例如下:

{
    "id": "5a3ce01f-1353-47ed-b355-0337dc890ff6", // 对话id 多组流式对话时靠它拼接
    "object": "chat.completion.chunk",
    "created": 1784792183,
    "model": "deepseek-v4-flash",
    "system_fingerprint": "fp_8b330d02d0_prod0820_fp8_kvcache_20260402",
    "choices": [
        {
            "index": 0,
            "delta": {
                "role": "assistant",
                "content": "" // 数据放在这里
            },
            "logprobs": null,
            "finish_reason": null
        }
    ]
}

现在根据chunk内容,list有值则拼接,无值则添加对话项(将id记录在项中用来寻找对应项)。

        for await (const chunk of res) {
            callback(prevList => {
                const chartIndexItem = prevList.findIndex(item => {
                    return item.charId === chunk.id
                })
                if(chartIndexItem == -1) {
                    return [...prevList, { ...chunk.choices[0].delta, charId: chunk.id}]
                }else {
                    return prevList.map(item => {
                        if(item.charId === chunk.id) {
                            return { ...item, content: item.content+chunk.choices[0].delta.content }
                        }else {
                            return item
                        }
                    })
                }
            })
        }

如此就实现了流式效果。

1.3.3 设置node中间层

上面是在前端项目中直接请求的openai接口,实际项目中需要使用node中间层做中转以保护key。
中转层在操作时需要注意一下几点:
1 发出请求时注意请求头的正确性

{
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  }

返回数据时使用res.write陆续返回。
数据返回结束后使用res.end();结束接口。
其他逻辑和前端项目中的内容等同,都是通过openai获取chund后对数据进行处理。

前端可以使用fetch读取流式数据(axios不支持)。
fetch读取流式数据的方式上面有介绍。使用response.body.getReader().read()读取内容,new TextDecoder().decode(value)进行解码。
response.body.getReader().read()反复读取直到返回的done为true时break。

这里也可以使用EventSource,但是EventSource仅支持get接口,不支持太长的上下文传递。

const eventSource = new EventSource(url);
eventSource.addEventListener("message", function(e: any) { content.value += e.data;});
eventSource.addEventListener('end', () => { eventSource.close();});

个人更推荐仍然使用openai来在前端读取,只是不写秘钥,把url改成node中间层的地址。

二 图像生成

我们仍然选用国内场景大模型:可灵ai来做示范。

可灵ai需要比文本请求多做一件事:通过jsonwebtoken包换取token。当然这一步在生产上肯定是后端做的,这里我们就放在前端了。
npm i jsonwebtoken安装依赖。

2.1 发布任务

可灵ai首页左侧有开发者平台按钮,开发者平台右上角能进入控制台(买个试用资源包,然后创建key)。
和文本一样,在header中写Authorization = “Bearer XXX”,其中 XXX 填写第一步获取的 API Key。
现在页面中准备好相关的内容。

<div style={{margin: '10px'}}>
    <Image
     width={200}
     src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
    />
</div>
<Space vertical size="medium">
     <Space.Compact style={{ width: '100%' }}>
          <Input defaultValue="Combine input and button" value={pInput} onChange={(e)=>setPInput(e.target.value)}/>
          <Button type="primary" onClick={() => getImage(pInput)}>图片生成</Button>
     </Space.Compact>
</Space>

然后在getImage事件中根据pInput的值获取图片,把图片url给image组件即可。
(api调用文档:https://klingai.com/document-api/api/image/2-1/image-generation)

const getImage = async (imageInput) => {
    const endPoint = 'https://api-beijing.klingai.com/v1/images/generations'
    let res = fetch(endPoint, {
        headers: {
            'Content-Type': 'application/json', 
            'Authorization': `Bearer xxxx`,
        },
        method: 'POST',
        body: JSON.stringify({
            prompt: imageInput, aspect_ratio: '1:1',
        })
    })
    if (res.status >= 400) { throw new Error(`Non-200 response: ${await res.text()}`); }
    const resJson = await res.json();
    const id = resJson.data.task_id;
    const resultUrl = `${endpoint}/${id}`;
    console.log(id, resultUrl)
    // while(true) {
        // 循环获取图片内容直到生成成功
        // 返回task_status标志位为succeed即成功
        // 成功后task_result中有生成图片的url
    // }
}

可灵ai是不支持跨域的,所以咋package.json中配置proxy来进行跨域。
在文本框中输入文字后点击图片生成id。

909536646579253261 /v1/images/generations/909536646579253261

根据这个id就可以来获取图片内容了。

2.2 获取图片

现在将while中的内容补全。

    while(true) {
        // console.log(resJson,id)
        // 循环获取图片内容直到生成成功
        // 返回task_status标志位为succeed即成功
        // 成功后task_result中有生成图片的url
        await new Promise((resolve) => setTimeout(resolve, 100)); // 预防堵塞 让出主线程
        const result = await fetch(resultUrl, { headers: {
            'Content-Type': 'application/json', 
            'Authorization': `Bearer xxxx`,
        }});
        
        const resultJson = await result.json();
        console.log(resultJson)
        const taskStatus = resultJson.data.task_status;
        if(taskStatus == 'succeed') {
            console.log(resultJson.data?.task_result.images[0].url)
            setImageUrl(resultJson.data?.task_result.images[0].url)
            return resultJson.data?.task_result.images[0].url
        }else {
            continue
        }
    }

setImageUrl是这个方法的第二个参数,用来赋值图片地址。
现在点击生成图片,页面上就出现了图片。
在这里插入图片描述

三 AI应用智能体

3.1 扣子智能体

我们先用扣子完成智能体的搭建。
打开网页https://code.coze.cn/home,点击左侧资源库,创建工作流。
工作流刚开始打开,是有开始和结束两个点,我们现在往上添加节点。
在这里插入图片描述
点击下方添加节点添加大模型节点,选择对应大模型,再将大模型节点与开始节点相连接,最后点击大模型节点,在右侧输入的部分将输入对接为开始的input,并选择根据一个模型。
这里我们先做一个给宝宝讲故事的智能体。
讲故事先要根据提示词生成故事,这里我们选择豆包1.5pro。
在输入下方,有系统提示词和用户提示词这两个input框,用户提示词就是用户输入的内容。我们使用{{input}}来直接透传即可(也可根据需要处理)。
系统提示词可以说是告诉ai去怎样处理用户提示词,这里我们需要告诉ai怎样生成故事。
这里我们重点讲智能体的搭建,怎样更好地写出系统提示词到后面去写,所以这里先给个示范即可。

# Task
分析用户输入,提取核心意图,输出一组便于搜索引擎精准匹配的 Query 词。

# Rules
1. 意图路由:
   - 经典童话/寓言(如三只小猪):直接提取故事标准名称 + "原文/绘本"。
   - 泛文化主题(如中秋节、龙):结合中国历史、神话传说,转化为“核心词 + 典故/传说”组合。
2. 格式要求:必须精炼,严禁包含“帮我找”、“请问”等口语化废话。
3. 兜底机制:若输入完全无意义或与儿童/文化/育儿无关,仅输出 ["无法识别的搜索意图"]# Output Format
- 必须且只能输出一个合法的 JSON 数组(包含 1-3 个字符串)。
- 严禁输出任何解释性文字、前缀或后缀。

然后再大模型节点的输出中增加querys项,类型选array。
将大模型的输出和结束节点的输出连接起来,点击试运行可以看到效果,是一组数据。
在这里插入图片描述既然是一组顺序,不难想到就需要循环调用。我们需要创建一个循环体。
点击添加节点/业务逻辑/循环,页面上出现循环和循环体节点,
将query输出和循环的输入链接。
在循环体中添加插件/谷歌搜索节点,将输入输出衔接好。
现在我们拿到了搜索结果,要整理搜索的内容。我们将新增一个大模型节点,依次设置好输入、输出以及系统提示词和用户提示词。
这个大模型节点主要负责写草本:
系统提示词:

你根据参考资料进行整理,撰写儿童故事草稿。

注意:儿童故事的内容深浅要符合6-8岁儿童的年龄和理解力,如果是经典故事,尽可能忠于原文,否则可适当自由创作。

用户提示词:

参考资料:{{input}}

其后可以再设置一个润色环节,然后将输入连接到输出就可以了。
可以看到整个智能体工作流的搭建其实就是将多个输入输出连接起来,根据不同的需要在适当环节选择不同智能体的过程。
在这里插入图片描述

点击试运行现在已经可以看到生成的故事了。
有了工作流,就相当于创建了一个灵魂,现在我们要创建一个基于该工作流的智能体。
先将工作流点击右上角发布下。
点击项目开发/新建项目/新建智能体。
在中间的编排面板中添加刚刚的工作流到智能体,后在左侧人设面板中输入:

根据用户输入的主题,调用bedtime_story工作流给孩子讲睡前故事。

右侧可以进行测试。
最后发布为api就可以通过api调用了。
实际生产中,扣子智能体一般用于原型搭建,一些情况下对隐私性和精密性有要求的,还是会选择使用nodejs中间层去搭建工作流的方式。

3.2 提示词工程

3.2.1 结构化输入和输出

提示词工程旨在提升大模型输出结果的质量,这里提供几个小技巧。
首先是结构化输入,在深度思考模式下,简单的输入也会被模型整理成较好的提示词,但是token耗费较大。
我们怎样直接给出一个好的提示词呢?就是使用结构词框架。
提示词框架(Prompt Framework)是指在大模型中,设计一组清晰且结构化的提示词,用以引导模型生成特定类型的输出。
最简单的结构词框架,就是角色(Role)- 任务(Task)- 结果(Result)。
举例:

你是一位拥有10年经验的资深职场自媒体操盘手和内容营销专家,擅长用极具网感和共鸣的语言,为职场新人拆解复杂的职场生存法则。
根据用户输入的职场痛点或话题,策划一篇高赞的自媒体推文大纲,要求:
1. 注重底层逻辑和思维认知,帮读者看透职场现象背后的本质;
2. 具有极强的实操性,提供拿来就能用的“话术模板”或“行动清单”;
3. 内容不要求长篇大论,要直击痛点,循序渐进地引导读者情绪;
4. 准备的案例真实且接地气(如:被领导画大饼、跨部门沟通甩锅),兼具代入感与实用性;
5. 拔高立意,在文末适当升华主题,传递积极向上的职场价值观。

结构化输出就是在提示词中, 指定输出为json结构并给出结构。

根据{{input}}的主题,用**中文**输出以下JSON格式内容:

{
 "story_instruction": "", 
"the_whole_story_content": "",
 "the_whole_story_translate_to_en": "", 
 "lessons": []
 }

大模型会自动解析每个字段含义,放置对应的内容进去。

{
"story_instruction": "讲述一个放羊娃多次撒谎喊 “狼来了”,最后狼真的来了却没人相信他的故事,教育人们要诚实,不能说谎。",
"the_whole_story_content": "从前,有个放羊娃,每天都去山上放羊...",
"the_whole_story_translate_to_en": "Once upon a time, ...",
"lessons": ["做人要诚实,不能通过说谎来达到自己的目的。", "如果经常说谎,当真正有危险需要帮助时,可能就不会有人相信和伸出援手。"]
}

结构化输出能得到比文本输出更优质的内容。
这边举个例子:

const getJSONData = async (setJsonData,stream=false) => {
    let res = await openai.chat.completions.create({
        messages: [{ role: "system", content: `
            角色:你是一名儿童绘本创作师,深耕多年儿童绘本,擅长用儿童绘本故事来引导矫正儿童不良习惯;
            目的:现在你需要创作输出绘本,来矫正两岁宝宝不好好睡觉的习惯;
            要求:1 绘本通俗易懂,适合两岁宝宝阅读 2 绘本不超过300字 3 绘本故事卡通风格,轻松可爱
            输出JSON格式的内容: {
                title:绘本的标题,需简短且充满童趣。
                brief:一句话或两句话的故事简述,方便在列表页展示。
                story_content:完整的故事纯文本,用于语音播报或文字阅读。
                target_age:建议阅读的年龄段。
                educational_value:该绘本传达的核心教育意义。
                tags:用于分类和检索的标签。
                pages:分页详情数组。对于绘本应用,通常需要拆分为单页展示,每页包含页码、单页文本以及用于 AI 绘画的提示词(image_prompt),方便后续直接调用生图接口。
            }
        ` }],
        model: "deepseek-v4-flash",
        thinking: {"type": "disabled"},
        stream,
    })

    console.log(JSON.parse(res.choices[0].message.content))
    setJsonData(res.choices[0].message.content)
}

得到的内容是:
在这里插入图片描述

3.2.2 分步思考工作流

分步思考工作流就是上面我们用扣子来实现的工作流,里面会有多个模型先后链接,分别处理查资料、大纲、草稿、润色等不同步骤。
使用分步思考工作流有个独特的地方,那就是可以将任务颗粒度细化,这种单点工作的模式可以取到更好地效果,就是更费token。

3.2.3 元提示和动态提示

元提示就是使用模型去生成提示词,然后用作下一模型的输入。
动态提示是可以根据输入的不同选用不同的提示词。
写一个简单的例子:

export default `今天的日期是 {{today}}针对今天的日期、节日、节气等信息,整理出以下内容:1. 今天的日期如果是特殊节日,介绍节日的文化和意义,以及节日的历史和背景。2. 今天的日期如果是特殊节气,介绍节气的文化和意义,以及节气的历史和背景。3. 今天的日期如果在历史上的重要事件中,介绍该事件的文化和意义,以及该事件的历史和背景。`;

然后用nunjucks进行模版解析生成最终提示词。

const prompt = nunjucks.renderString(systemPrompt, { today: today.value,});

-------------------------------------------------------------------------------------------------------------未完待续 持续更新

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐