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`
---- 上次 可以在
网页里- 和 大模型对话了
- 但是使用两个文件
- app.py
- 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生成一个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 后端转发方式的区别
| 对比项 | 前端直接调用 | 后端转发方式 |
|---|---|---|
| 架构复杂度 | 简单,减少了中间层 | 复杂,增加了后端服务层 |
| 响应速度 | 较快,减少了中转环节 | 较慢,增加了一次网络传输 |
| 安全性 | 较低,API密钥暴露在前端 | 较高,API密钥保存在服务端 |
| 开发效率 | 高,快速原型验证 | 低,需要额外开发后端接口 |
| 请求处理 | 受浏览器限制,有跨域和安全策略限制 | 不受浏览器限制,可处理复杂逻辑 |
| 适用场景 | 演示原型、个人项目、快速验证 | 生产环境、企业应用、需要安全控制的场景 |
- 这次 可以把代码整合成一个py文件
- 把index.html嵌入到app.py中
- 也可以 使用restful的方式直接向大模型提交请求
- 使用 纯html的方式
| 维度 | 纯 app.py | app.py + index.html |
|---|---|---|
| 核心特点 | 代码里“藏”页面,一锅炖 | 逻辑、页面分开,各管各 |
| 开发快慢 | 小需求快(不用切文件) | 大需求快(改界面不碰逻辑) |
| 好不好改 | 改界面=动代码,容易乱 | 改界面只动 HTML,很安全 |
| 适合场景 | 临时测、小工具(1-2个功能) | 正经 app、要加页面的项目 |
| 一句话总结 | 快但“乱”,适合临时用 | 清且“稳”,适合长期做 |
- 怎么让大模型返回的数据看起来更好看呢?🤔
- 下次再说👋
- 本文来自 oeasy Python 系统教程。
- 想完整、扎实学 Python,
- 搜索 oeasy 即可。











