在上篇中,笔者尝试构筑了一个简易的智能体聊天程序,虽然能够实现简单的对话功能,但由于是在集成开发环境(如Pycharm)下运行的,交互界面过于单调,离我们想要的聊天“搭子”还相差甚远。

        而在本文中,我们将实现前端与后端的联通,构建一个基于Flask的Web应用程序,使用户能在网页上进行与智能体的对话。

后端解析

        我们将后端代码文件命名为app_new.py,与上篇类似,我们需要构建一个QianfanChat类,包含以下功能:

  • 初始化方法__init__:初始化API的URL、API密钥、模型名称以及对话历史。如果提供了角色预设,则添加到系统消息中。
  • 添加系统消息add_system_message:将系统消息添加到对话历史。
  • 发送消息send_message:将用户消息添加到对话历史,然后向API发送请求,获取助手的消息并添加到对话历史。返回助手的消息。
  • 获取对话历史get_history:返回完整的对话历史。
  • 清空对话历史clear_history:仅保留系统消息,清空用户和助手的消息记录。

        接着,创建一个字典,用于存储每个会话的QianfanChat实例。

# 存储每个会话的聊天实例
chat_sessions = {}

        为了连接前端和后端的代码,我们需要以下的两个路由

        主页路由/

  • 如果用户会话中没有session_id,则生成一个新的UUID并将其存储在会话中。
  • 返回index_new.html模板。

        聊天页面路由/chat

  • 检查用户会话中是否定义了character_name,如果没有则重定向到主页。
  • 返回chat_new.html模板。
@app.route('/')
def index():
    """主页-角色设定"""
    if 'session_id' not in session:
        session['session_id'] = str(uuid.uuid4())
    return render_template('index_new.html')


@app.route('/chat')
def chat_page():
    """聊天页面"""
    if 'character_name' not in session:
        return redirect('/')
    return render_template('chat_new.html')

        图像生成API调用

        之前提到,我们希望额外添加以下功能:

        1、为智能体设置外观形象,即对话时的“头像”。

        2、设置用户与智能体的聊天背景。

        为了实现上述功能,笔者打算调用千帆大模型平台上的图像生成模型百度蒸汽机Air-Image(MuseSteamer-Air-Image)生成图片。

        相比文本生成模型,图像生成模型的body参数有所不同:
 

Body 参数:
model(string)(必选)

用于生图的模型。当前支持musesteamer-air-image。

prompt(string)(必选)

所需生成图片的描述。最大为 1000 个字符。

size(string)(可选)

只支持以下可选值,输入非可选值时,自动处理为1024*1024。
默认值为1024x1024。
可选值为:

  • 1024x1024
  • 1280x720
  • 720x1280
  • 1152x️864
  • 864x1152
  • 1328x1328
  • 1664x928
  • 928x1664
  • 1472x1104
  • 1104x1472
seed (integer)(可选)

随机种子。不设置时,自动生成随机数。取值范围为[0, 4294967295]

prompt_extend(string)(可选)

控制是否开启 prompt 增强。默认为true,开启。

response_format(string)(可选)

生成图像返回的格式。默认值为url,可填入urlb64_json。当填入b64_json返回图像的base64编码。

以下是一个示例代码:
 

import requests
import json
import random

def main():
    url = "https://qianfan.baidubce.com/v2/musesteamer/images/generations"

    payload = json.dumps({
        "prompt": "生成一张雪山的照片",
        "model": "musesteamer-air-image",
        "size": "1280x720",
        "seed": random.randint(0, 99999999),
        "prompt_extend": True,
        "response_format": "url"
    }, ensure_ascii=False)

    headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer bce-v3/ALTAK-1nkqtQRdhR5OnCXwno24R/4d57068ded4d9bd5af08f1ab27f045c93045c59e'
    }

    try:
        response = requests.post(url, headers=headers, data=payload.encode("utf-8"))
        response.raise_for_status()  # 如果响应状态码不是200,引发HTTPError异常
        print(response.text)
    except requests.exceptions.HTTPError as http_err:
        print(f"HTTP错误: {http_err}")
    except requests.exceptions.RequestException as req_err:
        print(f"请求错误: {req_err}")
    except Exception as err:
        print(f"其他错误: {err}")


if __name__ == '__main__':
    main()

        运行程序后,我们会得到一个url,点击即可查看生成的图片:

{"id":"as-s3wkby7h3a","created":1766055144,"data":[{"url":"http://qianfan-modelbuilder-img-gen.bj.bcebos.com/musesteamer-air-image/f8c40871b2074163b0b9aca24e55e3d2/f8c40871b2074163b0b9aca24e55e3d2/img-8f6f76ce-92db-4454-6688-af1c88310951.jpeg?authorization=bce-auth-v1%2FALTAKEDe5Wum0DnGzeUzScVC94%2F2025-12-18T10%3A52%3A31Z%2F86400%2Fhost%2Fcda97ea4d52d49fa34eb1f7081e2069f8ddaa6ddf82a43404fdffa905e5b8ff2"}]}

Process finished with exit code 0

        接下来,我们将具体实现以下两个功能:

        1、创建对话时的智能体头像。

        2、设置用户与智能体的聊天背景。

        我们使用同一张AI生成的图片,图片大小设置为1024x1024的正方形。我们对图像的中心区域进行圆形裁剪,作为“头像”。对于整张图像进行高斯模糊处理,作为“背景”。

       生成AI图片并处理路由/api/generate_image

  • 接收用户提供的描述文本,如果描述为空则返回错误。
  • 向百度千帆的API发送请求以生成描述中的图片。
  • 下载生成的图片并对其进行处理:背景模糊化,头像裁剪为圆形。
  • 将处理后的背景和头像保存为文件,并返回文件的URL路径

        以下是图像处理方面的部分代码:

@app.route('/api/generate_image', methods=['POST'])
def generate_image():
    """生成AI图片并处理"""
    try:
        data = request.json
        description = data.get('description', '').strip()

        if not description:
            return jsonify({'success': False, 'message': '描述不能为空'})

        # 调用百度千帆AI生成图片
        image_url = "https://qianfan.baidubce.com/v2/musesteamer/images/generations"
        payload = {
            "model": "musesteamer-air-image",
            "prompt": description,
            "size": "1024x1024",
            "seed": 42949672,
            "prompt_extend": True,
            "response_format": "url"
        }

        headers = {
            'Content-Type': 'application/json',
            'Authorization': f'Bearer {API_KEY}'
        }

        response = requests.post(image_url, headers=headers, json=payload, timeout=60)

        if response.status_code != 200:
            return jsonify({'success': False, 'message': f'图片生成失败: {response.status_code}'})

        result = response.json()
        if 'data' not in result or len(result['data']) == 0:
            return jsonify({'success': False, 'message': '未生成图片'})

        # 获取生成的图片URL
        generated_image_url = result['data'][0]['url']

        # 下载图片
        img_response = requests.get(generated_image_url, timeout=30)
        img = Image.open(BytesIO(img_response.content))

        # 处理背景图片(模糊效果)
        background = img.copy()
        background = background.filter(ImageFilter.GaussianBlur(radius=10))

        # 转换背景为base64(临时用于前端显示)
        bg_buffer = BytesIO()
        background.save(bg_buffer, format='PNG')
        bg_base64 = base64.b64encode(bg_buffer.getvalue()).decode()

        # 保存背景图片到文件
        bg_filename = save_image_to_file(f'data:image/png;base64,{bg_base64}', 'bg')

        # 处理头像(裁剪为圆形)
        width, height = img.size
        min_dim = min(width, height)
        radius = min_dim // 2
        center_x = width // 2
        center_y = height // 2

        # 创建圆形遮罩
        mask = Image.new('L', (radius * 2, radius * 2), 0)
        draw = ImageDraw.Draw(mask)
        draw.ellipse((0, 0, radius * 2, radius * 2), fill=255)

        # 裁剪中心区域
        avatar_crop = img.crop((center_x - radius, center_y - radius,
                                center_x + radius, center_y + radius))

        # 应用圆形遮罩
        avatar_output = Image.new('RGBA', (radius * 2, radius * 2), (0, 0, 0, 0))
        avatar_output.paste(avatar_crop, (0, 0))
        avatar_output.putalpha(mask)

        # 转换头像为base64(临时用于前端显示)
        av_buffer = BytesIO()
        avatar_output.save(av_buffer, format='PNG')
        av_base64 = base64.b64encode(av_buffer.getvalue()).decode()

        # 保存头像到文件
        av_filename = save_image_to_file(f'data:image/png;base64,{av_base64}', 'avatar')

        if not bg_filename or not av_filename:
            return jsonify({'success': False, 'message': '保存图片失败'})

        return jsonify({
            'success': True,
            'background': f'/user_images/{bg_filename}',
            'avatar': f'/user_images/{av_filename}'
        })

    except Exception as e:
        return jsonify({'success': False, 'message': f'处理失败: {str(e)}'})

            在早期的探索中,笔者将生成的图片base64数据存储在session中,导致session cookie超过了浏览器的4KB限制。base64编码的图片通常有几百KB到几MB,远超cookie的存储能力。

            于是有了以下报错:

    UserWarning: The 'session' cookie is too large: the value was 1119222 bytes but the header required 26 extra bytes. The final size was 1119248 bytes but the limit is 4093 bytes. Browsers may silently ignore cookies larger than this.

            解决方案:将图片数据存储在服务器端,session中只存储文件路径或ID。

    @app.route('/api/setup', methods=['POST'])
    def setup_character():
        """设置角色"""
        data = request.json
        character_name = data.get('character_name', '').strip()
        character_description = data.get('character_description', '').strip()
        background = data.get('background', '')
        avatar = data.get('avatar', '')
    
        if not character_name or not character_description:
            return jsonify({'success': False, 'message': '角色名称和描述不能为空'})
    
        # 保存到session(只保存文件路径)
        session['character_name'] = character_name
        session['character_description'] = character_description
        session['background'] = background  # 现在是文件路径
        session['avatar'] = avatar  # 现在是文件路径
    
        # 创建角色人设
        character_preset = (
            f"你是{character_name}。{character_description}\n"
            f"请始终以{character_name}的身份和口吻回应。"
        )
    
        # 创建聊天实例
        session_id = session['session_id']
        chat_sessions[session_id] = QianfanChat(
            api_key=API_KEY,
            model=MODEL,
            character_preset=character_preset
        )
    
        return jsonify({'success': True})

    前端解析

            本节会侧重说明JavaScript部分用于实现agent的创建与对话的代码。CSS部分主要负责页面的样式设计,包括布局、颜色、字体等,非项目重点,故此部分省略。

    index_new.html

            主要功能是提供一个用户界面,让用户可以设置AI聊天角色的基本信息(名称和人设),并通过描述生成角色的形象图片(背景和头像),最后将设定的角色信息传递给后端并开始聊天。其核心部分定义了两个异步函数:generateImage 和 startChat,分别用于生成角色形象图片和开始聊天。

    1. 变量声明

      let backgroundUrl = '';
      let avatarUrl = '';
      

      声明了两个变量用于存储生成的背景图片和头像图片的URL。

    2. generateImage函数

      async function generateImage() {
          const description = document.getElementById('imageDescription').value.trim();
          const generateBtn = document.getElementById('generateBtn');
          const loadingMsg = document.getElementById('loadingMsg');
          const errorMsg = document.getElementById('errorMsg');
          const successMsg = document.getElementById('successMsg');
      
          if (!description) {
              errorMsg.textContent = '请输入人物形象描述';
              setTimeout(() => errorMsg.textContent = '', 3000);
              return;
          }
      
          generateBtn.disabled = true;
          loadingMsg.style.display = 'block';
          errorMsg.textContent = '';
          successMsg.textContent = '';
      
          try {
              const response = await fetch('/api/generate_image', {
                  method: 'POST',
                  headers: {'Content-Type': 'application/json'},
                  body: JSON.stringify({description})
              });
      
              const data = await response.json();
      
              if (data.success) {
                  backgroundUrl = data.background;
                  avatarUrl = data.avatar;
      
                  const bgPreview = document.getElementById('backgroundPreview');
                  const avPreview = document.getElementById('avatarPreview');
      
                  bgPreview.src = backgroundUrl;
                  bgPreview.style.display = 'block';
                  avPreview.src = avatarUrl;
                  avPreview.style.display = 'block';
      
                  successMsg.textContent = '✅ 生成成功!';
              } else {
                  errorMsg.textContent = '生成失败:' + data.message;
              }
          } catch (error) {
              errorMsg.textContent = '请求失败:' + error.message;
          } finally {
              generateBtn.disabled = false;
              loadingMsg.style.display = 'none';
          }
      }
      
      • 获取用户输入的人物形象描述。
      • 如果描述为空,则显示错误提示信息。
      • 否则,禁用“生成图片”按钮并显示加载提示信息。
      • 发送POST请求到/api/generate_image接口,请求参数为description
      • 如果请求成功且返回successtrue,则更新backgroundUrlavatarUrl并显示预览图片及成功提示。
      • 如果请求失败或返回successfalse,则显示错误提示信息。
      • 最后,无论结果如何,重新启用“生成图片”按钮并隐藏加载提示信息。
    3. startChat函数

      async function startChat() {
          const characterName = document.getElementById('characterName').value.trim();
          const characterDescription = document.getElementById('characterDescription').value.trim();
          const errorMsg = document.getElementById('errorMsg');
      
          if (!characterName || !characterDescription) {
              errorMsg.textContent = '请填写角色名称和描述';
              setTimeout(() => errorMsg.textContent = '', 3000);
              return;
          }
      
          try {
              const response = await fetch('/api/setup', {
                  method: 'POST',
                  headers: {'Content-Type': 'application/json'},
                  body: JSON.stringify({
                      character_name: characterName,
                      character_description: characterDescription,
                      background: backgroundUrl,
                      avatar: avatarUrl
                  })
              });
      
              const data = await response.json();
      
              if (data.success) {
                  window.location.href = '/chat';
              } else {
                  errorMsg.textContent = '设置失败:' + data.message;
              }
          } catch (error) {
              errorMsg.textContent = '请求失败:' + error.message;
          }
      }
      
      • 获取用户输入的角色名称和描述。
      • 如果名称或描述为空,则显示错误提示信息。
      • 否则,发送POST请求到/api/setup接口,请求参数包括角色名称、描述、背景图片URL和头像图片URL。
      • 如果请求成功且返回successtrue,则跳转到/chat页面开始聊天。
      • 如果请求失败或返回successfalse,则显示错误提示信息。

    chat_new.html

            这部分代码实现了一个简单的在线聊天界面,用户可以向智能体发送消息,智能体会返回回复。功能包括动态加载和显示消息、禁用发送按钮以防止重复发送、以及清空对话历史和返回设置的功能。主要功能是在用户和服务器之间进行消息传递,并在用户界面上动态展示这些消息。详细的函数解析如下:

    • 全局变量

      • let isLoading = false;:用于判断当前是否正在发送消息。
    • window.onload

      • 页面加载完成后执行的函数。它会通过fetch API向服务器请求会话数据,并根据返回的数据更新头像、角色名称和背景。
    • handleKeyPress

      • 监听键盘事件,当用户按下Enter键且当前没有正在发送的消息时,调用sendMessage()函数。
    • sendMessage

      • 获取输入框中的消息内容,并将其发送到服务器。消息发送成功后,会将服务器返回的消息内容显示在聊天界面中。在发送消息的过程中,禁用发送按钮并显示加载状态,发送完成后恢复发送按钮的可使用状态并隐藏加载状态。
    • addMessage

      • 动态创建消息元素,并将其添加到聊天消息容器中。根据消息发送者(用户或助手)的不同,为消息元素添加不同的样式。
    • clearChat

      • 清空对话历史。在调用此函数前会弹出确认框,用户确认后会向服务器发送请求清空对话历史,并在成功后清空聊天消息容器中的内容。
    • backToSetup

      • 返回设置页面。调用此函数前会弹出确认框,用户确认后会跳转到主页,并清空当前聊天对话。
        <script>
            let isLoading = false;
    
            // 页面加载时获取会话数据
            window.onload = async function() {
                try {
                    const response = await fetch('/api/get_session_data');
                    const data = await response.json();
    
                    if (data.success) {
                        document.getElementById('characterName').textContent = data.character_name;
    
                        // 如果有头像,设置头像
                        if (data.avatar) {
                            document.getElementById('headerAvatar').src = data.avatar;
                        }
    
                        // 如果有背景,设置背景
                        if (data.background) {
                            document.body.style.backgroundImage = `url('${data.background}')`;
                        }
                    }
                } catch (error) {
                    console.error('获取会话数据失败:', error);
                }
            };
    
            function handleKeyPress(event) {
                if (event.key === 'Enter' && !isLoading) {
                    sendMessage();
                }
            }
    
            async function sendMessage() {
                const input = document.getElementById('messageInput');
                const message = input.value.trim();
    
                if (!message || isLoading) return;
    
                // 显示用户消息
                addMessage(message, 'user');
                input.value = '';
    
                // 显示加载状态
                isLoading = true;
                document.getElementById('sendBtn').disabled = true;
                document.getElementById('loading').style.display = 'block';
    
                try {
                    const response = await fetch('/api/send_message', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({message})
                    });
    
                    const data = await response.json();
    
                    if (data.success) {
                        addMessage(data.response, 'assistant');
                    } else {
                        addMessage('发送失败:' + data.message, 'assistant');
                    }
                } catch (error) {
                    addMessage('请求失败:' + error.message, 'assistant');
                } finally {
                    isLoading = false;
                    document.getElementById('sendBtn').disabled = false;
                    document.getElementById('loading').style.display = 'none';
                }
            }
    
            function addMessage(content, role) {
                const messagesDiv = document.getElementById('chatMessages');
                const messageDiv = document.createElement('div');
                messageDiv.className = `message ${role}`;
    
                const contentDiv = document.createElement('div');
                contentDiv.className = 'message-content';
                contentDiv.textContent = content;
    
                messageDiv.appendChild(contentDiv);
                messagesDiv.appendChild(messageDiv);
    
                // 滚动到底部
                messagesDiv.scrollTop = messagesDiv.scrollHeight;
            }
    
            async function clearChat() {
                if (!confirm('确定要清空对话历史吗?')) return;
    
                try {
                    const response = await fetch('/api/clear_history', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'}
                    });
    
                    const data = await response.json();
    
                    if (data.success) {
                        document.getElementById('chatMessages').innerHTML = '';
                    }
                } catch (error) {
                    console.error('清空对话失败:', error);
                }
            }
    
            function backToSetup() {
                if (confirm('返回设置页面将清空当前对话,确定要返回吗?')) {
                    window.location.href = '/';
                }
            }
        </script>

    实际应用

            让我们总结一下目录结构。

    project_root/
    │
    ├── app_new.py                 # Flask 后端主文件
    │
    └───templates/                 # HTML 模板文件夹
        ├── index_new.html        # 主页 - 角色设定页面
        └── chat_new.html         # 聊天页面

            接下来,我们运行app_new.py文件。

     * Serving Flask app 'app_new'
     * Debug mode: on
    WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
     * Running on http://127.0.0.1:5000
    Press CTRL+C to quit
     * Restarting with stat
     * Debugger is active!
     * Debugger PIN: 819-357-877

            它会返回一个网址,点进去就是聊天界面,需要用户自行输入角色名称、角色描述以及人物形象描述。

            我们根据提示输入自己想要的描述,点击“生成图片”按钮就可以创建智能体的形象。

            随后,我们点击“开始聊天按钮”,进入与智能体的对话界面。

            可以看到,得益于ERNIE模型强大的中文自然语言处理能力,智能体出色地遵循了我们设置的人物设定,对话内容拥有符合描述的风格及语气;同时由蒸汽机模型绘制出的具体人物形象也拉近了用户与Agent的距离,就好像我们在与一个活生生的人在社交媒体上聊天,这让我们创建的智能体成为了一个合格的聊天“搭子”!

            以上,就是这两篇博客的全部内容,感谢以往的学姐学长给与的灵感,也感谢各位读者能阅读到最后。笔者经验及能力尚且稚嫩,不足之处请多多包含。

    写在最后的小彩蛋:

            本文示例中的Agent其实不是笔者自己编出来的哦,实际上“阿花”她还有另一个更广为人知的名字——字节跳动旗下AI助手——豆包。(或者应该说,是豆包的prototype在泛娱乐领域的分身?)

            完。

    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐