Skip to content

Latest commit

 

History

History
719 lines (588 loc) · 25.8 KB

File metadata and controls

719 lines (588 loc) · 25.8 KB
Error in user YAML: (<unknown>): could not find expected ':' while scanning a simple key at line 3 column 1
---
- oeasy Python 0781
- 这是 oeasy 系统化 Python 教程,从基础一步步讲,扎实、完整、不跳步。愿意花时间学,就能真正学会。
本教程同步发布在: 

     个人网站: `https://oeasy.org` 
     蓝桥云课: `https://www.lanqiao.cn/courses/3584` 
     GitHub: `https://github.com/overmind1980/oeasy-python-tutorial` 
     Gitee: `https://gitee.com/overmind1980/oeasypython` 
---

文生图模型调用

总结

  • 上次 可以在 网页里
    • 和 大模型对话了
  • 但是使用两个文件
    1. app.py
    2. index.html

图片描述

  • 可以把他俩整合起来吗?🤔

网页应用

  • 把index.html整合进入app.py
from flask import Flask, request, Response
from flask_cors import CORS
from openai import OpenAI
import json

app = Flask(__name__)
CORS(app)

# 从llm.py集成的show_messages函数
def show_messages(messages):
    print("==========消息开始==============")
    counter = 1
    for message in messages:
        print("\033[4" + str(counter) + "m", end="")
        print(message, end="")
        print("\33[0m")
        counter = counter + 1
        if counter == 7:
            counter = 1
    print("==========消息结束==============")

# 使用llm.py中的API配置
client = OpenAI(
    base_url='https://api-inference.modelscope.cn/v1',
    api_key='ms-81c1f87a-fa0a-4edc-a4a5-4bc7ba3cbbba',
)

@app.route('/')
def index():
    # 将HTML内容分解为多个字符串片段,避免转义问题
    html_head = '''<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>对话</title>
    <style>
      body { margin: 0; padding: 20px; font-family: Arial; background-color: white; }
      #messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; background-color: white; }
      .user, .assistant { margin: 5px 0; padding: 8px; border-radius: 5px; }
      .user { text-align: right; background: #e3f2fd; }
      .assistant { background: #f5f5f5; }
      .system { text-align: center; background: #fff3e0; padding: 5px; margin: 5px 0; font-style: italic; color: #795548; }
      input { width: 75%; padding: 10px; border: 1px solid #ccc; }
      button { padding: 10px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>'''
    
    html_body = '''
    <div id="messages"></div>
    <input type="text" id="input" placeholder="八戒,有何话要说?">
    <button id="sendBtn" onclick="sendMessage()">发送</button>

    <script>
      function sendMessage() {
        var msg = document.getElementById('input').value.trim();
        if (!msg) return;
        
        var btn = document.getElementById('sendBtn');
        btn.disabled = true;
        btn.textContent = '发送中...';
        
        addMessage(msg, 'user');
        document.getElementById('input').value = '';
        
        // 为助手消息准备一个容器
        var assistantDiv = document.createElement('div');
        assistantDiv.className = 'assistant';
        document.getElementById('messages').appendChild(assistantDiv);
        document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
        
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/api/chat', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        
        // 处理流式响应
        xhr.onreadystatechange = function() {
          if (xhr.readyState == 3) {
            // 获取当前响应文本
            var chunk = xhr.responseText;
            // 分割成多行
            var lines = chunk.split('\\n');
            
            // 清空现有内容,因为responseText包含所有已接收的数据
            assistantDiv.textContent = '';
            
            for (var i = 0; i < lines.length; i++) {
              var line = lines[i].trim();
              if (line.startsWith('data: ')) {
                try {
                  var dataStr = line.substring(6);
                  if (dataStr) {
                    var data = JSON.parse(dataStr);
                    if (data.content) {
                      // 添加内容
                      assistantDiv.textContent += data.content;
                      document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
                    }
                    if (data.done) {
                      // 完成响应
                      btn.disabled = false;
                      btn.textContent = '发送';
                    }
                    if (data.error) {
                      // 处理错误
                      assistantDiv.textContent = '错误: ' + data.error;
                      btn.disabled = false;
                      btn.textContent = '发送';
                    }
                  }
                } catch (e) {
                  console.error('解析错误:', e);
                }
              }
            }
          } else if (xhr.readyState == 4) {
            // 请求完成
            btn.disabled = false;
            btn.textContent = '发送';
          }
        };
        
        xhr.send(JSON.stringify({message: msg}));
      }
      
      function addMessage(content, type) {
        var div = document.createElement('div');
        div.className = type;
        div.textContent = content;
        document.getElementById('messages').appendChild(div);
        document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
      }
      
      // 监听回车事件
      document.getElementById('input').addEventListener('keypress', function(e) {
        if (e.key === 'Enter') {
          sendMessage();
        }
      });
    </script>
</body>
</html>'''
    
    # 合并所有HTML片段
    return html_head + html_body

@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.get_json()
    user_message = data.get('message', '')
    
    # 构建消息列表,使用llm.py中的系统消息
    messages = [
        {"role": "system", "content": "你是孙悟空,我是猪八戒"},
        {"role": "user", "content": user_message}
    ]
    
    # 在控制台输出消息
    show_messages(messages)
    
    def generate():
        try:
            response = client.chat.completions.create(
                model='Qwen/Qwen3-Next-80B-A3B-Instruct',
                messages=messages,
                stream=True
            )
            
            for chunk in response:
                if chunk.choices[0].delta.content:
                    content = chunk.choices[0].delta.content
                    # 在控制台输出响应内容
                    print(content, end='', flush=True)
                    yield "data: " + json.dumps({"content": content}) + "\n\n"
            
            print()  # 输出一个换行符
            yield "data: " + json.dumps({"done": True}) + "\n\n"
            
        except Exception as e:
            print("OpenAI API Error:", str(e))
            yield "data: " + json.dumps({"error": str(e)}) + "\n\n"
    
    return Response(generate(), mimetype='text/plain')

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=8080)

角色再总结

图片描述

  • 后台数据

图片描述

  • 悟空八戒的人设不出现在对话中
    • 但是 对大模型的回答会有影响
角色 核心定位 作用与特点 典型使用场景举例
system 系统 1. 提前设定模型
2. 不直接参与对话,用户看不到
3. 影响整个对话的回答风格和方向
你是孙悟空
我是猪八戒
user 对话需求发起者 1. 用户的问题
2. 直接触发模型
3. 来生成回应的核心
你对取经怎么看?
  • 可以把悟空、八戒的名字写清楚吗?

写清楚

图片描述

from flask import Flask, request, Response
from flask_cors import CORS
from openai import OpenAI
import json

app = Flask(__name__)
CORS(app)

# 从llm.py集成的show_messages函数
def show_messages(messages):
    print("==========消息开始==============")
    counter = 1
    for message in messages:
        print("\033[4" + str(counter) + "m", end="")
        print(message, end="")
        print("\33[0m")
        counter = counter + 1
        if counter == 7:
            counter = 1
    print("==========消息结束==============")

# 使用llm.py中的API配置
client = OpenAI(
    base_url='https://api-inference.modelscope.cn/v1',
    api_key='ms-81c1f87a-fa0a-4edc-a4a5-4bc7ba3cbbba',
)

@app.route('/')
def index():
    # 将HTML内容分解为多个字符串片段,避免转义问题
    html_head = '''<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>对话</title>
    <style>
      body { margin: 0; padding: 20px; font-family: Arial; background-color: white; }
      #messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; background-color: white; }
      .user, .assistant, .system { margin: 10px 0; }
      .message-header { font-size: 12px; font-weight: bold; margin-bottom: 2px; }
      .message-content { padding: 8px; border-radius: 5px; word-wrap: break-word; }
      .user .message-header { text-align: right; color: #1976D2; }
      .assistant .message-header { color: #4CAF50; }
      .user .message-content { text-align: right; background: #e3f2fd; }
      .assistant .message-content { background: #f5f5f5; }
      .system { text-align: center; background: #fff3e0; padding: 5px; margin: 5px 0; font-style: italic; color: #795548; }
      input { width: 75%; padding: 10px; border: 1px solid #ccc; }
      button { padding: 10px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>'''
    
    html_body = '''
    <div id="messages"></div>
    <input type="text" id="input" placeholder="八戒,有何话要说?">
    <button id="sendBtn" onclick="sendMessage()">发送</button>

    <script>
      function sendMessage() {
        var msg = document.getElementById('input').value.trim();
        if (!msg) return;
        
        var btn = document.getElementById('sendBtn');
        btn.disabled = true;
        btn.textContent = '发送中...';
        
        addMessage(msg, 'user');
        document.getElementById('input').value = '';
        
        // 为助手消息准备一个容器
        var assistantContainer = document.createElement('div');
        assistantContainer.className = 'assistant';
        
        var assistantHeader = document.createElement('div');
        assistantHeader.className = 'message-header';
        assistantHeader.textContent = '悟空';
        
        var assistantMessage = document.createElement('div');
        assistantMessage.className = 'message-content';
        
        assistantContainer.appendChild(assistantHeader);
        assistantContainer.appendChild(assistantMessage);
        
        document.getElementById('messages').appendChild(assistantContainer);
        document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
        
        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/api/chat', true);
        xhr.setRequestHeader('Content-Type', 'application/json');
        
        // 处理流式响应
        xhr.onreadystatechange = function() {
          if (xhr.readyState == 3) {
            // 获取当前响应文本
            var chunk = xhr.responseText;
            // 分割成多行
            var lines = chunk.split('\\n');
            
            // 清空现有内容,因为responseText包含所有已接收的数据
            assistantMessage.textContent = '';
            
            for (var i = 0; i < lines.length; i++) {
              var line = lines[i].trim();
              if (line.startsWith('data: ')) {
                try {
                  var dataStr = line.substring(6);
                  if (dataStr) {
                    var data = JSON.parse(dataStr);
                    if (data.content) {
                    // 添加内容
                    assistantMessage.textContent += data.content;
                    document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
                  }
                    if (data.done) {
                      // 完成响应
                      btn.disabled = false;
                      btn.textContent = '发送';
                    }
                    if (data.error) {
                    // 处理错误
                    assistantMessage.textContent = '错误: ' + data.error;
                    btn.disabled = false;
                    btn.textContent = '发送';
                  }
                  }
                } catch (e) {
                  console.error('解析错误:', e);
                }
              }
            }
          } else if (xhr.readyState == 4) {
            // 请求完成
            btn.disabled = false;
            btn.textContent = '发送';
          }
        };
        
        xhr.send(JSON.stringify({message: msg}));
      }
      
      function addMessage(content, type) {
        var container = document.createElement('div');
        container.className = type;
        
        var header = document.createElement('div');
        header.className = 'message-header';
        header.textContent = type === 'user' ? '八戒' : type === 'assistant' ? '悟空' : '系统';
        
        var message = document.createElement('div');
        message.className = 'message-content';
        message.textContent = content;
        
        container.appendChild(header);
        container.appendChild(message);
        
        document.getElementById('messages').appendChild(container);
        document.getElementById('messages').scrollTop = document.getElementById('messages').scrollHeight;
      }
      
      // 监听回车事件
      document.getElementById('input').addEventListener('keypress', function(e) {
        if (e.key === 'Enter') {
          sendMessage();
        }
      });
    </script>
</body>
</html>'''
    
    # 合并所有HTML片段
    return html_head + html_body

@app.route('/api/chat', methods=['POST'])
def chat():
    data = request.get_json()
    user_message = data.get('message', '')
    
    # 构建消息列表,使用llm.py中的系统消息
    messages = [
        {"role": "system", "content": "你是孙悟空,我是猪八戒"},
        {"role": "user", "content": user_message}
    ]
    
    # 在控制台输出消息
    show_messages(messages)
    
    def generate():
        try:
            response = client.chat.completions.create(
                model='Qwen/Qwen3-Next-80B-A3B-Instruct',
                messages=messages,
                stream=True
            )
            
            for chunk in response:
                if chunk.choices[0].delta.content:
                    content = chunk.choices[0].delta.content
                    # 在控制台输出响应内容
                    print(content, end='', flush=True)
                    yield "data: " + json.dumps({"content": content}) + "\n\n"
            
            print()  # 输出一个换行符
            yield "data: " + json.dumps({"done": True}) + "\n\n"
            
        except Exception as e:
            print("OpenAI API Error:", str(e))
            yield "data: " + json.dumps({"error": str(e)}) + "\n\n"
    
    return Response(generate(), mimetype='text/plain')

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=8080)

图片描述

  • app.py + html vs 纯app.py

app.py

图片描述

纯网页解决问题

根据app.py生成一个index.html,使用fetch API与大模型通信,纯html解决问题

图片描述

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>悟空与八戒的对话</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); overflow: hidden; }
        .header { background: #e57373; color: white; padding: 10px; text-align: center; font-weight: bold; }
        .chat-container { height: 400px; overflow-y: auto; padding: 15px; }
        .message { margin-bottom: 10px; padding: 10px; border-radius: 8px; max-width: 75%; word-wrap: break-word; }
        .message .role { font-weight: bold; font-size: 12px; margin-bottom: 5px; }
        .user { background: #ffe8c1; margin-left: auto; }
        .user .role { color: #c62828; }
        .ai { background: #e3f2fd; margin-right: auto; }
        .ai .role { color: #0d47a1; }
        .input-group { display: flex; padding: 10px; border-top: 1px solid #eee; }
        input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px 0 0 4px; }
        button { padding: 10px 20px; background: #e57373; color: white; border: none; border-radius: 0 4px 4px 0; cursor: pointer; }
        button:disabled { background: #ccc; }
        .typing { padding: 10px; color: #999; }
        .status { text-align: center; padding: 5px; font-size: 12px; color: #666; border-top: 1px solid #eee; }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">悟空与八戒的取经对话</div>
        <div class="chat-container" id="chat"></div>
        <div class="input-group">
            <input type="text" id="input" placeholder="八戒,有何话要说?" disabled>
            <button id="send" disabled>发送</button>
        </div>
        <div class="status" id="status">正在召唤悟空...</div>
    </div>

    <script>
        const API_CONFIG = {
            base_url: 'https://api-inference.modelscope.cn/v1',
            api_key: 'ms-32a7d288-877e-43db-8ff6-1410c61fdee0',
            model: 'deepseek-ai/DeepSeek-V3.1'
        };

        const chat = document.getElementById('chat');
        const input = document.getElementById('input');
        const send = document.getElementById('send');
        const status = document.getElementById('status');
        let isProcessing = false;

        document.addEventListener('DOMContentLoaded', () => {
            appendSystemMessage('大师兄和二师兄已在取经路上...');
            appendMessage('呆子!你又有什么话要说?是不是又想偷懒耍滑了?', 'ai');
            input.disabled = false;
            send.disabled = false;
            status.textContent = '悟空已就绪,八戒可以说话了';
        });

        function appendMessage(text, type) {
            const msg = document.createElement('div');
            msg.className = 'message ' + type;
            
            const roleLabel = document.createElement('div');
            roleLabel.className = 'role';
            roleLabel.textContent = type === 'user' ? '八戒' : '悟空';
            msg.appendChild(roleLabel);
            
            const content = document.createElement('div');
            content.textContent = text;
            msg.appendChild(content);
            
            chat.appendChild(msg);
            chat.scrollTop = chat.scrollHeight;
        }

        function appendSystemMessage(text) {
            const msg = document.createElement('div');
            msg.style.textAlign = 'center';
            msg.style.color = '#999';
            msg.style.fontSize = '12px';
            msg.textContent = text;
            chat.appendChild(msg);
            chat.scrollTop = chat.scrollHeight;
        }

        function showTyping() {
            const typing = document.createElement('div');
            typing.className = 'typing';
            typing.id = 'typing';
            typing.textContent = 'AI正在回复...';
            chat.appendChild(typing);
            chat.scrollTop = chat.scrollHeight;
        }

        function hideTyping() {
            const typing = document.getElementById('typing');
            if (typing) typing.remove();
        }

        async function sendMessage() {
            const text = input.value.trim();
            if (!text || isProcessing) return;

            input.value = '';
            isProcessing = true;
            send.disabled = true;
            appendMessage(text, 'user');
            status.textContent = '发送中...';
            showTyping();

            try {
                const requestBody = {
                    model: API_CONFIG.model,
                    messages: [
                        { role: 'system', content: '你现在是孙悟空,花果山水帘洞美猴王,齐天大圣。你的性格特点是:聪明机灵、顽皮好动、爱捉弄猪八戒但内心关心他,说话带有西游记中孙悟空的语气,经常自称"俺老孙"。用户现在是猪八戒,你需要用孙悟空的身份和语气与他对话。' },
                        { role: 'user', content: text }
                    ],
                    stream: true
                };

                const response = await fetch(`${API_CONFIG.base_url}/chat/completions`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_CONFIG.api_key}` },
                    body: JSON.stringify(requestBody)
                });

                if (!response.ok) throw new Error('API错误');

                const reader = response.body.getReader();
                const decoder = new TextDecoder();
                let reply = '';

                while (true) {
                    const { done, value } = await reader.read();
                    if (done) break;
                    const chunk = decoder.decode(value, { stream: true });
                    const lines = chunk.split('\n');
                    
                    for (const line of lines) {
                        if (line.startsWith('data: ') && line !== 'data: [DONE]') {
                            try {
                                const json = JSON.parse(line.slice(6));
                                reply += json.choices[0].delta.content || '';
                            } catch (e) {}
                        }
                    }
                }

                hideTyping();
                appendMessage(reply || '暂无回复', 'ai');
                status.textContent = '就绪';
            } catch (error) {
                hideTyping();
                appendSystemMessage('发送失败,请重试');
                status.textContent = '错误';
            } finally {
                isProcessing = false;
                send.disabled = false;
            }
        }

        send.addEventListener('click', sendMessage);
        input.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); });
    </script>
</body>
</html>

运行网页

python -m http.server 8080

图片描述

  • 尝试运行
python3 -m http.server 8080 &
firefox localhost:8080

效果

技术要点 说明
RESTful API 设计 大模型服务提供标准的HTTP接口
支持浏览器直接发送请求和接收响应
CORS 跨域机制 服务端配置了跨域资源共享策略
允许来自浏览器的跨域请求
Bearer Token 认证 通过HTTP头中的Authorization字段传递API密钥进行身份验证
Fetch API/Axios 前端使用这些API发起异步HTTP请求到大模型服务
流式响应处理 支持Stream模式,通过ReadableStream API处理实时返回的内容

图片描述

  • 都是 整合成一个文件
    • html vs python
    • 哪个好呢?

对比

图片描述

  • 前后端 有啥 区别 呢?

图片描述

前端 vs 后端

  • 前端直接获取 vs 后端转发方式的区别
对比项 前端直接调用 后端转发方式
架构复杂度 简单,减少了中间层 复杂,增加了后端服务层
响应速度 较快,减少了中转环节 较慢,增加了一次网络传输
安全性 较低,API密钥暴露在前端 较高,API密钥保存在服务端
开发效率 高,快速原型验证 低,需要额外开发后端接口
请求处理 受浏览器限制,有跨域和安全策略限制 不受浏览器限制,可处理复杂逻辑
适用场景 演示原型、个人项目、快速验证 生产环境、企业应用、需要安全控制的场景

总结

  • 这次 可以把代码整合成一个py文件
    • 把index.html嵌入到app.py中
  • 也可以 使用restful的方式直接向大模型提交请求
    • 使用 纯html的方式

Flask 两种结构对比(极简表格版)

维度 纯 app.py app.py + index.html
核心特点 代码里“藏”页面,一锅炖 逻辑、页面分开,各管各
开发快慢 小需求快(不用切文件) 大需求快(改界面不碰逻辑)
好不好改 改界面=动代码,容易乱 改界面只动 HTML,很安全
适合场景 临时测、小工具(1-2个功能) 正经 app、要加页面的项目
一句话总结 快但“乱”,适合临时用 清且“稳”,适合长期做

图片描述

  • 怎么让大模型返回的数据看起来更好看呢?🤔
  • 下次再说👋

  • 本文来自 oeasy Python 系统教程。
  • 想完整、扎实学 Python,
  • 搜索 oeasy 即可。