在 Google Colab 中运行 Streamlit 应用的方法与本地不同。Colab 本身是一个 Jupyter Notebook 环境,直接在单元格里运行 Streamlit 代码不会像在本地那样自动弹出浏览器窗口。
需要借助一个叫做 ngrok
(或者类似的隧道服务,如 localtunnel
) 的工具,它可以在 Colab 服务器和你本地的浏览器之间建立一个安全的连接通道,让你能访问到在 Colab 上运行的 Streamlit 应用。pyngrok
是 ngrok
的一个 Python 封装库,使用起来更方便。
以下是在 Colab 中运行 Streamlit 应用的步骤:
步骤:
- 将 Streamlit 应用代码写入一个
.py
文件中: 使用 Colab 的 “magic command”%%writefile
。 - 安装必要的库:
streamlit
和pyngrok
。 - 获取 ngrok Authtoken(推荐): 为了获得更稳定和更长时间的连接,建议注册一个免费的 ngrok 账户并获取 Authtoken。
- 配置 ngrok 并启动 Streamlit 应用: 使用
pyngrok
启动隧道,并后台运行 Streamlit 服务。 - 访问应用:
pyngrok
会提供一个公共 URL,可以通过这个 URL 在浏览器中访问应用。
请按顺序在 Colab 的代码单元格中执行以下操作:
单元格 1:将 Streamlit 应用代码写入 app.py
文件
将之前修正好的、包含 DeepSeek API 密钥和所有逻辑的完整 Streamlit 应用代码粘贴到下面 %%writefile app.py
之后。
%%writefile app.py
import os
import streamlit as st
from dotenv import load_dotenv
from langchain_core.prompts import (
ChatPromptTemplate,
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
)
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
from langchain_core.exceptions import OutputParserException
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Dict, Any, Iterator
import json
import traceback # 用于打印更详细的错误堆栈
# --- 配置常量 ---
# 警告:不建议在生产或共享代码中硬编码API密钥!
DASHSCOPE_API_KEY = "sk-xxx" # 提供的API Key
DASHSCOPE_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
DEFAULT_MODEL_NAME = "qwen-max"
SYSTEM_MESSAGE_CONTENT = "你是一个协助用户润色文章的人工智能助手。"
load_dotenv()
# --- Pydantic 模型 ---
class Paragraph(BaseModel):
original_paragraph: str = Field(description="原始段落")
edited_paragraph: str = Field(description="改进后的段落")
feedback: str = Field(description="对原始段落的建设性反馈")
# --- 全局变量用于LLM实例和初始化错误 ---
llm_init_error_message: str | None = None
deterministic_llm: ChatOpenAI | None = None
creative_llm: ChatOpenAI | None = None
# --- LLM 工厂与初始化 ---
def initialize_llms():
global deterministic_llm, creative_llm, llm_init_error_message
try:
if not DASHSCOPE_BASE_URL:
llm_init_error_message = "DashScope API Base URL 未配置 (硬编码值为空)。"
print(f"错误: {llm_init_error_message}")
return
if not DASHSCOPE_API_KEY:
llm_init_error_message = "DashScope API Key 未配置 (硬编码值为空)。"
print(f"错误: {llm_init_error_message}")
return
if not DASHSCOPE_API_KEY.startswith("sk-"): # 基本的key格式检查
llm_init_error_message = f"DashScope API Key 格式似乎不正确 (硬编码值: {DASHSCOPE_API_KEY[:5]}...)"
print(f"错误: {llm_init_error_message}")
return
deterministic_llm = ChatOpenAI(
model=DEFAULT_MODEL_NAME,
temperature=0.0,
base_url=DASHSCOPE_BASE_URL,
api_key=DASHSCOPE_API_KEY,
)
creative_llm = ChatOpenAI(
model=DEFAULT_MODEL_NAME,
temperature=0.9,
base_url=DASHSCOPE_BASE_URL,
api_key=DASHSCOPE_API_KEY,
)
# 尝试进行一次简单调用以验证API Key和连接 (可选,但有助于早期发现问题)
# print("尝试与LLM进行测试性连接...")
# test_response = deterministic_llm.invoke("Hello")
# print(f"LLM测试连接成功: {test_response.content[:50]}...")
except Exception as e:
llm_init_error_message = f"初始化语言模型(DashScope Qwen-Max)时发生错误: {e}\n{traceback.format_exc()}"
print(llm_init_error_message)
# 清理可能部分初始化的实例
deterministic_llm = None
creative_llm = None
initialize_llms() # 应用启动时初始化LLMs
# --- 通用提示模板 ---
system_prompt_template = SystemMessagePromptTemplate.from_template(SYSTEM_MESSAGE_CONTENT)
# --- JSON 解析和验证函数 ---
def parse_json_string_to_paragraph(json_string: str) -> Paragraph:
try:
cleaned_json_string = json_string.strip()
if cleaned_json_string.startswith("```json"):
cleaned_json_string = cleaned_json_string[7:]
if cleaned_json_string.endswith("```"):
cleaned_json_string = cleaned_json_string[:-3]
data = json.loads(cleaned_json_string.strip())
return Paragraph(**data)
except json.JSONDecodeError as e:
error_msg = f"LLM输出的不是有效的JSON格式。错误: {e}. 收到的内容: '{json_string[:300]}...'"
print(error_msg)
raise OutputParserException(error_msg) from e
except Exception as e:
error_msg = f"JSON数据未能通过Paragraph模型验证。错误: {e}. 收到的内容: '{json_string[:300]}...'"
print(error_msg)
raise OutputParserException(error_msg) from e
# --- 核心功能函数 ---
def generate_title(article: str) -> Iterator[str]:
if not creative_llm:
yield f"错误: 创意型语言模型(Qwen-Max)未初始化。请检查应用启动日志。{llm_init_error_message or ''}"
return
user_prompt_template = HumanMessagePromptTemplate.from_template(
"""你的任务是为一篇文章起一个标题。以下是文章内容供你参考:
---
{article}
---
标题应基于文章的内容创作。要富有创意,但标题必须清晰、吸引人,且与文章主题密切相关。
只能输出文章标题,不允许有任何解释或其他文字。"""
)
title_prompt = ChatPromptTemplate.from_messages([system_prompt_template, user_prompt_template])
title_chain = title_prompt | creative_llm | StrOutputParser()
try:
for chunk in title_chain.stream({"article": article}):
yield chunk
except Exception as e:
error_detail = f"生成标题时发生错误: {e}\n{traceback.format_exc()}"
print(error_detail)
yield f"抱歉,生成标题时发生内部错误。详情请查看应用日志。错误概要: {e}"
def generate_description(article: str) -> Iterator[str]:
if not deterministic_llm:
yield f"错误: 确定性语言模型(Qwen-Max)未初始化。请检查应用启动日志。{llm_init_error_message or ''}"
return
user_prompt_template = HumanMessagePromptTemplate.from_template(
"""你的任务是为这篇文章撰写一段描述。文章内容如下:
---
{article}
---
请输出一段符合 SEO 友好的文章描述。除描述内容外,不要输出任何其他内容。"""
)
description_prompt = ChatPromptTemplate.from_messages([system_prompt_template, user_prompt_template])
description_chain = description_prompt | deterministic_llm | StrOutputParser()
try:
for chunk in description_chain.stream({"article": article}):
yield chunk
except Exception as e:
error_detail = f"生成描述时发生错误: {e}\n{traceback.format_exc()}"
print(error_detail)
yield f"抱歉,生成描述时发生内部错误。详情请查看应用日志。错误概要: {e}"
def optimize_article_paragraph(article: str) -> Iterator[Dict[str, str]]:
if not creative_llm:
yield {"错误": f"创意型语言模型(Qwen-Max)未初始化。请检查应用启动日志。{llm_init_error_message or ''}"}
return
user_prompt_template = HumanMessagePromptTemplate.from_template(
"""你的任务是评审并修改所提供文章中的一个段落。以下是供你参考的文章内容:
---
{article}
---
请从中选择一个段落进行评审和修改。
在修改过程中,请确保向用户提供有建设性的反馈,以便他们了解如何提升自己的写作。
请严格按照以下JSON格式输出回复,确保JSON格式正确无误,不要包含任何其他说明文字或Markdown代码块标记(如 ```json ... ```):
{{
"original_paragraph": "这里是原始段落内容",
"edited_paragraph": "这里是编辑和改进后的段落内容",
"feedback": "这里是对原始段落的具体、建设性的反馈和改进建议"
}}
"""
)
optimize_prompt = ChatPromptTemplate.from_messages([system_prompt_template, user_prompt_template])
optimize_chain = (
optimize_prompt
| creative_llm
| StrOutputParser()
| RunnableLambda(parse_json_string_to_paragraph)
| (lambda p_object: {
"原始段落": p_object.original_paragraph,
"优化后段落": p_object.edited_paragraph,
"优化建议": p_object.feedback,
})
)
try:
# stream对于返回单个完整对象的链可能行为特殊,确保它能正确处理
# 通常.stream() 用于处理分块的文本流。如果链返回的是单个字典,可能直接 .invoke() 更合适
# 但为了与 write_stream 保持一致,并且假设LangChain的 .stream() 对这类 Runnable 也能正确处理单个对象流
for item_dict in optimize_chain.stream({"article": article}):
yield item_dict # 期望这里流式传输单个字典
except OutputParserException as ope:
error_detail = f"优化段落时解析输出失败: {ope}\n{traceback.format_exc()}"
print(error_detail)
yield {"错误": f"解析LLM输出失败,可能是模型未按要求返回JSON。错误概要: {ope}"}
except Exception as e:
error_detail = f"优化段落时发生错误: {e}\n{traceback.format_exc()}"
print(error_detail)
yield {"错误": f"优化段落时发生内部错误。详情请查看应用日志。错误概要: {e}"}
# --- Streamlit 应用主逻辑 ---
if llm_init_error_message:
st.error(f"应用无法启动,因为语言模型初始化失败:\n{llm_init_error_message}")
st.error("请检查硬编码的API Key和Base URL是否正确,API Key是否有调用`qwen-max`模型的权限及足够配额,以及网络连接。详细错误信息已打印到Colab控制台/日志。")
else:
st.set_page_config(page_title="AI 写作助手 (Qwen-Max)", layout="wide")
st.title("📝 AI 写作助手 (Qwen-Max)")
st.caption("输入您的文章,AI 将帮助生成标题、描述并优化段落。")
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if "results" in message and message["results"]:
# 确保在显示前检查各项结果是否存在
title_output = message["results"].get("title_output")
description_output = message["results"].get("description_output")
optimization_output = message["results"].get("optimization_output")
if title_output:
st.subheader("✍️ 建议标题")
# 检查是否为错误信息
if isinstance(title_output, str) and title_output.startswith("错误:") or title_output.startswith("抱歉,"):
st.error(title_output)
else:
st.markdown(title_output)
if description_output:
st.subheader("✍️ SEO 描述")
if isinstance(description_output, str) and description_output.startswith("错误:") or description_output.startswith("抱歉,"):
st.error(description_output)
else:
st.markdown(description_output)
if optimization_output:
st.subheader("✍️ 段落优化与建议")
if isinstance(optimization_output, dict) and "错误" in optimization_output:
st.error(f"段落优化失败: {optimization_output['错误']}")
elif isinstance(optimization_output, dict):
st.write(optimization_output)
else: # 可能是字符串错误
st.error(str(optimization_output))
user_input = st.chat_input("请输入您的文章或段落...")
if user_input:
st.session_state.messages.append({"role": "user", "content": user_input})
with st.chat_message("user"):
st.markdown(user_input)
with st.chat_message("assistant"):
# 再次检查LLM状态,以防万一
if llm_init_error_message or not deterministic_llm or not creative_llm:
error_content = f"抱歉,处理请求失败,因为语言模型未正确加载。错误: {llm_init_error_message or '未知初始化错误'}"
st.error(error_content)
st.session_state.messages.append({
"role": "assistant", "content": error_content, "results": {}
})
else:
# --- 生成标题 ---
st.subheader("✍️ 建议标题")
title_container = st.empty()
title_full_response = ""
try:
with st.spinner("正在生成标题..."):
for chunk in generate_title(user_input):
title_full_response += chunk
title_container.markdown(title_full_response)
except Exception as e: # 捕获在st.spinner内部可能发生的其他错误
error_detail = f"生成标题UI时发生意外错误: {e}\n{traceback.format_exc()}"
print(error_detail)
title_container.error(f"生成标题时发生界面错误。详情请查看日志。概要: {e}")
title_full_response = f"生成标题时发生界面错误: {e}"
# --- 生成描述 ---
st.subheader("✍️ SEO 描述")
description_container = st.empty()
description_full_response = ""
try:
with st.spinner("正在生成描述..."):
for chunk in generate_description(user_input):
description_full_response += chunk
description_container.markdown(description_full_response)
except Exception as e:
error_detail = f"生成描述UI时发生意外错误: {e}\n{traceback.format_exc()}"
print(error_detail)
description_container.error(f"生成描述时发生界面错误。详情请查看日志。概要: {e}")
description_full_response = f"生成描述时发生界面错误: {e}"
# --- 优化段落 ---
st.subheader("✍️ 段落优化与建议")
optimization_container = st.empty()
optimization_response = {} # 初始化
try:
with st.spinner("正在优化段落..."):
final_optimization_dict = {}
# optimize_article_paragraph 现在返回一个字典流 (期望是单个字典)
for item_dict in optimize_article_paragraph(user_input):
final_optimization_dict = item_dict # 捕获最后一个 (或唯一的) 字典
optimization_container.write(final_optimization_dict) # 实时显示流内容
optimization_response = final_optimization_dict # 存储最终结果
# 再次检查流本身是否返回了错误字典
if isinstance(optimization_response, dict) and "错误" in optimization_response:
optimization_container.error(f"优化段落时发生错误: {optimization_response['错误']}")
except Exception as e: # 捕获在st.spinner内部可能发生的其他错误
error_detail = f"优化段落UI时发生意外错误: {e}\n{traceback.format_exc()}"
print(error_detail)
optimization_container.error(f"优化段落时发生界面错误。详情请查看日志。概要: {e}")
optimization_response = {"错误": f"优化段落时发生界面错误: {e}"}
st.session_state.messages.append({
"role": "assistant",
"content": "我已经针对您的文章生成了以下内容 (使用 Qwen-Max):",
"results": {
"title_output": title_full_response,
"description_output": description_full_response,
"optimization_output": optimization_response,
}
})
# st.rerun() # 如果需要立即刷新整个聊天记录
单元格 2:安装库
!pip install -q streamlit pyngrok
单元格 3:配置 ngrok 并启动 Streamlit 应用
from pyngrok import ngrok, conf
import os
import getpass # 用于隐藏 Authtoken 输入
# --- ngrok 配置 ---
# 1. 访问 https://dashboard.ngrok.com/get-started/your-authtoken 注册并获取您的 ngrok Authtoken。
# 2. 将获取到的 Authtoken 粘贴到下面的输入框中。
print("为了获得更稳定的ngrok连接,建议输入您的ngrok Authtoken。")
print("请访问 https://dashboard.ngrok.com/get-started/your-authtoken 获取。")
NGROK_AUTH_TOKEN = getpass.getpass("请输入您的 ngrok Authtoken (直接回车跳过,但不推荐): ")
if NGROK_AUTH_TOKEN:
conf.get_default().auth_token = NGROK_AUTH_TOKEN
print("Ngrok Authtoken 已配置。")
else:
print("警告:未配置 ngrok Authtoken,连接可能不稳定或受限。")
# --- 运行 Streamlit 应用 ---
# 确保之前的ngrok和streamlit进程被杀死,以防重复运行单元格导致端口冲突
!pkill -f "ngrok"
!pkill -f "streamlit run app.py" # 更精确地杀死目标进程
# 使用 nohup 和 & 在后台运行 Streamlit,并将输出重定向到日志文件
# --server.headless=true 和 --server.enableCORS=false 有助于在云环境中运行
# --server.port 8501 指定端口,ngrok将连接到此端口
print("正在后台启动 Streamlit 应用...")
get_ipython().system_raw("nohup streamlit run app.py --server.headless true --server.enableCORS false --server.port 8501 &> streamlit.log &")
# 等待几秒钟让 Streamlit 服务器启动
import time
time.sleep(5)
# --- 启动 ngrok 隧道 ---
try:
# 连接到 Streamlit 默认运行的 8501 端口
public_url = ngrok.connect(8501, proto="http")
print("------------------------------------------------------------------------------------")
print(f"您的 Streamlit 应用正在运行!请通过以下公共链接访问:")
print(public_url)
print("------------------------------------------------------------------------------------")
print("注意:此链接在Colab会话激活期间有效。关闭此Notebook或会话超时将导致链接失效。")
print("如果应用没有立即加载,请稍等片刻或刷新页面。")
print("如果遇到问题,可以查看 streamlit.log 文件获取 Streamlit 应用的输出日志。")
print("要停止应用和ngrok隧道,可以中断此单元格的执行,或在Colab中“运行时”->“中断执行”,然后“终止会话”。")
except Exception as e:
print(f"Ngrok 连接失败: {e}")
print("可能的原因及解决方法:")
print("1. Ngrok Authtoken 未设置或无效:请确保您输入了正确的Authtoken。")
print("2. Colab 环境的网络限制或 ngrok 服务暂时不可用:稍后重试。")
print("3. Streamlit 应用未能成功启动:请检查上面 `%%writefile app.py` 单元格中的代码,并查看 `streamlit.log` 文件。")
print(" 要查看日志,可以在新的Colab单元格中运行 `!cat streamlit.log`")
如何使用:
- 运行单元格 1: 这会将您的 Streamlit 代码保存到
app.py
文件中。请确保您已经将正确的、完整的 Streamlit 代码(包括API密钥等)粘贴到该单元格中。 - 运行单元格 2: 安装
streamlit
和pyngrok
。 - 运行单元格 3:
- 它会提示您输入 ngrok Authtoken。您可以从 ngrok Dashboard 获取。输入后按回车。如果您跳过此步骤(直接按回车),ngrok 仍然可以工作,但连接可能不稳定或有时间限制。
- 然后它会在后台启动您的 Streamlit 应用 (
app.py
)。 - 接着,它会启动 ngrok 隧道,并打印出一个
.ngrok.io
或.ngrok-free.app
的URL。
- 访问应用: 点击输出的那个公共 URL,就应该能在浏览器中看到并使用您的 Streamlit 应用了。
重要提示:
- API 密钥: 您在
app.py
中硬编码的DASHSCOPE_API_KEY
必须是有效的。 - Ngrok Authtoken: 强烈建议使用 Authtoken。
- 会话持续时间: ngrok 链接只在您的 Colab 会话处于活动状态时有效。如果 Colab 笔记本关闭或会话超时,链接将失效,您需要重新运行单元格 3。
- 日志: Streamlit 应用的输出(包括潜在的错误)会被重定向到
streamlit.log
文件。如果在访问 URL 时遇到问题,可以在新的 Colab 单元格中运行!cat streamlit.log
来查看日志。 - 停止: 要停止应用和 ngrok 隧道,您可以中断单元格 3 的执行,或者在 Colab 菜单中选择“运行时”->“中断执行”,更彻底的方式是“终止会话”。
- LLM 初始化错误处理: 我在
app.py
的代码中加入了对 LLM 初始化失败的检查,如果模型无法加载,Streamlit 应用会显示错误信息而不是直接崩溃。
这样操作后,应该就能在 Colab 中成功运行并访问您的 Streamlit 应用了。