在 UniApp 中实现stream流式输出 AI 聊天功能,AI输出内容用Markdown格式展示

在 UniApp 中实现流式 AI 聊天功能

介绍

在现代 Web 开发中,流式 API 响应能够显著提升用户体验,尤其是在与 AI 聊天接口进行交互时。本文将介绍如何在 UniApp 中使用 Fetch API 实现一个流式响应的 AI 聊天功能,包括实时更新聊天内容和滚动到底部的功能。

实现

用 Markdown 格式展示 AI 输出的内容

<!-- index.vue -->
<view v-else class="bot-message" :key="'bot-msg-' + index">
  <view class="avatar-container">
    <image
      class="message-avatar"
      src="/static/images/icon_robot.png"
    />
  </view>
  <view class="message-content bot-bubble">
    <u-loading-icon
      v-if="
        isSendLoading &&
        index == chatMessages.length - 1 &&
        !getText(message.content)
      "
    ></u-loading-icon>
    <text v-else>
        // 用Markdown格式展示
      <AiMarkdownViewer
        class="message-text"
        :content="message.content"
      />
      <!-- {{ message.content || '服务异常,请重试' }} -->
    </text>
  </view>
</view>


<!-- AiMarkdownViewer.vue -->
// 使用showdown插件
<template>
	<view
		class="markdown-container"
		v-html="parsedContent"
		ref="markdownContainer"
	></view>
</template>

<script>
import showdown from 'showdown'

export default {
	name: 'AiMarkdownViewer',
	props: {
		content: {
			type: String,
			required: true,
		},
	},
	data() {
		return {
			converter: new showdown.Converter({
				tables: true,
				tasklists: true,
				simplifiedAutoLink: true,
				strikethrough: true,
				extensions: [this.tableEnhancement()],
			}),
		}
	},
	computed: {
		parsedContent() {
			if (this.content) {
				const processed = this.content.replace(
					/^```markdown\n([\s\S]*?)\n```$/gm, // 添加m标志处理多行
					(match, content) => {
						return content
					}
				)
				return this.converter.makeHtml(processed)
			} else {
				return '微警灌云还在学习中,请您咨询当地派出所'
			}
		},
	},
	methods: {
		tableEnhancement() {
			return {
				type: 'output',
				filter: (text) => {
					// 为表格添加容器和样式类
					return text
						.replace(
							/<table>/g,
							'<div class="table-wrapper"><table class="data-table">'
						)
						.replace(/<\/table>/g, '</table></div>')
						.replace(/<td>/g, '<td class="data-cell">')
						.replace(/<th>/g, '<th class="header-cell">')
				},
			}
		},
	},
}
</script>
<style scoped lang="scss">
.markdown-container {
	margin: 0 auto;
	// padding: 20px;
	width: 100%;
	font-size: 26rpx;
	line-height: 1.7;
	color: #374151;
  white-space: normal; 
}
/deep/ {
	.table-wrapper {
		overflow-x: auto;
		margin: 1em 0;
		border: 1px solid #ebeef5;
		border-radius: 4px;
		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
		.data-table {
			width: 100%;
			min-width: 600px;
			border-collapse: collapse;
			font-size: 28rpx;
			line-height: 1.5;

			.header-cell {
				background-color: #f8f9fa;
				color: #606266;
				font-weight: 600;
				padding: 12px 16px;
				border-bottom: 2px solid #ebeef5;
				white-space: nowrap;
			}

			.data-cell {
				padding: 12px 16px;
				border-bottom: 1px solid #ebeef5;
				color: #606266;
				min-width: 80px;

				&:empty::before {
					content: ' ';
					display: inline-block;
					width: 1px;
				}
			}

			tr:hover {
				background-color: #f5f7fa;
			}

			tr:nth-child(even) {
				background-color: #fafafa;
			}

			td:first-child,
			th:first-child {
				border-left: 1px solid #ebeef5;
			}

			td:last-child,
			th:last-child {
				border-right: 1px solid #ebeef5;
			}
		}
	}
}
/deep/ {
	h1,
	h2,
	h3,
	h4,
	h5,
	h6 {
		margin: 1em 0;
		font-weight: 600;
		color: #1f2937;

		&:not(h1) {
			border-bottom: 0.5px solid #e5e7eb;
			padding-bottom: 0.4em;
		}
	}

	h1 {
		font-size: 1.5em;
	}
	h2 {
		font-size: 1.4em;
	}
	h3 {
		font-size: 1.3em;
	}
	h4 {
		font-size: 1.2em;
	}
	h5 {
		font-size: 1.1em;
	}
	h6 {
		font-size: 1em;
	}

	// 列表样式
	ul,
	ol {
		margin: 0.8em 0;
		padding-left: 1.5em;

		li {
			margin: 0.4em 0;
			&::marker {
				color: #6b7280;
			}
		}
	}

	// 分割线
	hr {
		border: 0;
		height: 0.5px;
		background: #e5e7eb;
		margin: 1.5em 0;
	}

	// 代码块
	pre {
		background-color: #f9fafb;
		border-radius: 8px;
		padding: 1em;
		margin: 1.2em 0;
		border: 0.5px solid #e5e7eb;

		code {
			font-family: 'JetBrains Mono', Consolas, monospace;
			font-size: 13px;
			color: #374151;
			line-height: 1.6;
		}
	}

	// 行内代码
	code:not(pre code) {
		background-color: #f3f4f6;
		padding: 0.2em 0.4em;
		border-radius: 4px;
		font-size: 0.9em;
		color: #dc2626;
	}

	// 引用块
	blockquote {
		border-left: 3px solid #e5e7eb;
		margin: 1em 0;
		padding: 0.5em 1em;
		color: #4b5563;
		background-color: #f8fafc;
		border-radius: 0 4px 4px 0;
	}

	// 链接
	a {
		color: #3b82f6;
		text-decoration: none;
		&:hover {
			text-decoration: underline;
		}
	}
}
/deep/ {
	pre {
		code {
			display: block;
			width: 100%;
			overflow-x: auto;
		}
	}
}
</style>

我们需要使用 Fetch API 向 AI 聊天服务发送请求,并读取其流式响应。以下是实现的关键代码段。

async getAIChat(params, aiMessage) {  
    // 使用 Fetch API 进行流式请求  
    const response = await fetch(`${config.aiBaseUrl}/api/v1/chat/completions`, {  
        method: 'POST',  
        headers: {  
            Authorization: `Bearer ${config.apikey}`,  
            'Content-Type': 'application/json',  
        },  
        body: JSON.stringify(params),  
    });  

    // 检测响应状态  
    if (!response.ok) {  
        const errorData = await response.json();  
        throw new Error(errorData.message || '请求失败');  
    }  

    this.isSendLoading = false; // 更新加载状态  

    // 使用 response.body.getReader() 开始逐块读取流式响应,使用 UTF-8 编码解码数据。
    const reader = response.body.getReader();  
    const decoder = new TextDecoder('utf-8');  
    let done = false;  

    // 用于流式读取数据  
    while (!done) {  
        const { done: readerDone, value } = await reader.read();  
        done = readerDone;  

        if (value) {  
            // 逐块解码并拼接  
            const chunkText = decoder.decode(value, { stream: !done });  
            // 解析接收到的 JSON 数据  
            const lines = chunkText.split('\n'); // 按行分割  

            lines.forEach((line) => {  
                if (line.startsWith('data:')) {  
                    const jsonString = line.substring(5).trim();  
                    try {  
                        const jsonData = JSON.parse(jsonString);  

                        // 提取内容部分  
                        const content = jsonData.choices?.[0]?.delta?.content || '';  

                        if (content) {  
                            aiMessage.content += content; // 更新聊天内容  

                            // 清除开头的换行符  
                            if (aiMessage.content.startsWith('\n')) {  
                                aiMessage.content = aiMessage.content.slice(1);  
                            }  

                            // 刷新页面渲染  
                            this.$forceUpdate(); // 确保视图更新  

                            // 滚动到底部  
                            this.$nextTick(() => {  
                                this.scrollToBottom(); // 调用滚动到底部的方法  
                            });  
                        }  
                    } catch (error) {  
                        console.error('Error parsing JSON:', error);  
                    }  
                }  
            });  
        }  
    }  
}  
<think>好的,我现在需要帮助用户了解如何使用Streamlit构建对话框并调用DeepSeek大模型实现流式输出。首先,我得确认自己对这两个技术点的理解是否正确。 Streamlit是一个用于快速构建数据应用的Python框架,特别适合机器学习模型的展示。用户提到“构建对话框”,可能指的是在网页应用中创建一个聊天界面,让用户输入信息,然后与模型交互。而DeepSeek大模型,我需要确认其API是否支持流式输出,比如类似OpenAI的逐字返回结果的方式。 接下来,我应该分步骤来设计解决方案。首先是搭建Streamlit的基本界面,包括输入框和显示区域。然后,集成DeepSeek的API,处理身份验证和请求发送。流式输出的部分可能需要使用Streamlit的特定功能,比如st.write_stream或者自定义生成器函数,来逐步显示模型返回的内容。 需要检查Streamlit是否支持异步请求,或者是否需要使用生成器来实现流式效果。另外,API调用的部分,可能需要查阅DeepSeek的文档,确认其流式传输的支持情况,比如是否有类似OpenAIstream参数。 还有安全方面的问题,比如API密钥的管理,应该提醒用户使用Streamlit的secrets功能来存储密钥,避免硬编码在代码中。此外,错误处理也需要考虑,比如网络请求失败时的反馈。 可能遇到的挑战包括流式数据的正确处理,确保每接收到一个数据块就即时显示,而不会等到全部完成。此外,界面布局的美观性,比如聊天历史记录的显示,可能需要使用st.session_state来维护对话状态,确保每次交互后历史记录更新。 最后,测试整个流程,确保从用户输入到模型响应,再到流式输出的每个环节都正常工作。如果有问题,需要逐步排查,比如检查API响应结构,确认Streamlit的组件是否正确使用。</think>以下是使用Streamlit+DeepSeek构建流式对话应用的实现指南(分步骤说明): ```python import streamlit as st from deepseek_api import DeepSeek # 假设提供的SDK import os # 初始化配置 def init_config(): st.set_page_config(page_title="DeepSeek Chat") if "messages" not in st.session_state: st.session_state.messages = [] # 流式响应生成器 def stream_generator(prompt): api_key = os.getenv("DEEPSEEK_API_KEY") # 从环境变量读取密钥 client = DeepSeek(api_key=api_key) # 假设API支持流式模式 response_stream = client.chat.completions.create( model="deepseek-chat", messages=[{"role": "user", "content": prompt}], stream=True # 启用流式传输 ) for chunk in response_stream: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content # 界面布局 def main(): init_config() # 显示历史消息 for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # 用户输入处理 if prompt := st.chat_input("请输入您的问题"): # 添加用户消息 st.session_state.messages.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) # 生成AI回复 with st.chat_message("assistant"): response = st.write_stream(stream_generator(prompt)) st.session_state.messages.append({"role": "assistant", "content": response}) if __name__ == "__main__": main() ``` 关键实现步骤说明: 1. **环境准备**: - 安装依赖:`pip install streamlit deepseek-sdk` - 设置API密钥:`export DEEPSEEK_API_KEY='your-api-key'` 2. **核心机制**: - **流式处理**:通过生成器函数逐块获取API响应 - **会话记忆**:使用`st.session_state`保持对话历史 - **即时渲染**:`st.write_stream()`实现逐字输出效果 3. **注意事项**: - 确保使用支持流式传输的API终端节点 - 需要处理网络中断等异常情况 - 可通过`st.spinner()`添加等待指示器 - 建议添加速率限制防止滥用 4. **扩展方向**: ```python # 添加功能示例: with st.sidebar: st.selectbox("模型版本", ["v1.0", "v2.0"]) st.slider("温度参数", 0.0, 1.0, 0.7) if st.button("清空历史"): st.session_state.messages = [] ``` 实际部署时需注意: 1. 通过`streamlit run app.py`启动服务 2. 生产环境建议添加身份验证 3. 可结合`st.cache_data`实现缓存优化 4. 需要处理API调用频率限制 请根据实际API文档调整参数设置,最新SDK使用方法建议参考官方文档。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值