用Colab启动Streamlit应用

在 Google Colab 中运行 Streamlit 应用的方法与本地不同。Colab 本身是一个 Jupyter Notebook 环境,直接在单元格里运行 Streamlit 代码不会像在本地那样自动弹出浏览器窗口。

需要借助一个叫做 ngrok (或者类似的隧道服务,如 localtunnel) 的工具,它可以在 Colab 服务器和你本地的浏览器之间建立一个安全的连接通道,让你能访问到在 Colab 上运行的 Streamlit 应用。pyngrokngrok 的一个 Python 封装库,使用起来更方便。

以下是在 Colab 中运行 Streamlit 应用的步骤:

步骤:

  1. 将 Streamlit 应用代码写入一个 .py 文件中: 使用 Colab 的 “magic command” %%writefile
  2. 安装必要的库: streamlitpyngrok
  3. 获取 ngrok Authtoken(推荐): 为了获得更稳定和更长时间的连接,建议注册一个免费的 ngrok 账户并获取 Authtoken。
  4. 配置 ngrok 并启动 Streamlit 应用: 使用 pyngrok 启动隧道,并后台运行 Streamlit 服务。
  5. 访问应用: 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. 运行单元格 1: 这会将您的 Streamlit 代码保存到 app.py 文件中。请确保您已经将正确的、完整的 Streamlit 代码(包括API密钥等)粘贴到该单元格中。
  2. 运行单元格 2: 安装 streamlitpyngrok
  3. 运行单元格 3:
    • 它会提示您输入 ngrok Authtoken。您可以从 ngrok Dashboard 获取。输入后按回车。如果您跳过此步骤(直接按回车),ngrok 仍然可以工作,但连接可能不稳定或有时间限制。
    • 然后它会在后台启动您的 Streamlit 应用 (app.py)。
    • 接着,它会启动 ngrok 隧道,并打印出一个 .ngrok.io.ngrok-free.app 的URL。
  4. 访问应用: 点击输出的那个公共 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 应用了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

frostmelody

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值