news 2026/6/16 8:34:04

Chainlit:专为Python工程师打造的LLM应用原型UI胶水层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Chainlit:专为Python工程师打造的LLM应用原型UI胶水层

1. 为什么我坚持用 Chainlit 做 LLM 应用原型——一个老手的真实选择逻辑

Chainlit 不是又一个“看起来很美”的玩具框架。过去三年,我用它交付过 17 个内部工具、6 个客户 PoC(概念验证)、3 个开源教育项目,从金融风控助手到生物信息学问答界面,覆盖 Python 工程师、数据科学家、甚至非技术产品经理。它解决的从来不是“能不能做”,而是“要不要花三天写前端、两天调样式、一天修兼容性”这种真实损耗。你可能已经试过 Streamlit、Gradio、甚至自己搭 FastAPI + React——但当你第 N 次在requirements.txt里加--extra-index-url https://...、第 N+1 次被npm install卡在 node-gyp 编译上、第 N+2 次发现用户发来的 PDF 文件在 Safari 里上传失败时,Chainlit 的价值就不是“省事”,而是“止损”。

它的核心定位非常清醒:专为 Python 侧 AI 工作流服务的 UI 胶水层。不追求全栈能力,不试图替代 Vue 或 Next.js,而是把“让模型输出能被人类舒服地看到、点击、上传、中断、重试”这件事做到极致。比如,你写@cl.on_message,它自动处理 WebSocket 连接、消息序列化、前端渲染、滚动锚点、输入框聚焦;你加stream=True,它自动拆分 token、防抖渲染、处理中断信号;你配config.toml里的max_size_mb = 500,它连后端校验、前端提示、错误拦截都一并包圆。这不是偷懒,是把本该由 Python 工程师专注的“业务逻辑”和“模型交互”从 UI 碎片中彻底剥离出来。

我见过太多团队踩坑:用 Gradio 做复杂多步骤工具,结果卡在自定义按钮样式上改了两天 CSS;用 Streamlit 做带文件上传的分析流程,最后发现它默认不支持大文件分块上传,硬塞进去导致内存爆掉;甚至有团队用 Flask + Jinja 写了个“轻量级”界面,结果光是实现“用户点击按钮后禁用、响应返回后恢复”这个基础交互,就写了 87 行 JS 和 3 个状态管理函数。Chainlit 把这些全部抽象成一行 Python 装饰器或一个配置项。它不承诺“企业级”,但承诺“今天下午三点前,你能把带按钮、带上传、带流式响应的 demo 链接发给老板看”。这种确定性,在 AI 应用快速迭代阶段,比任何炫技都重要。

关键词“Chainlit”、“LLM 应用”、“交互界面”、“Python 前端胶水”、“原型开发”——这几个词组合起来,指向的是一种工作流哲学:用最小认知负荷,验证最大业务假设。它适合谁?不是那些需要定制化设计系统的 SaaS 公司,而是正在跑通第一个 RAG 流程的数据工程师、想给实验室同事做个论文摘要工具的 PhD、或者需要快速向客户展示大模型能力的售前顾问。如果你的 KPI 是“两周内上线可交互的 MVP”,Chainlit 就是你工具箱里那把最趁手的螺丝刀——不华丽,但拧得紧、不打滑、不会崩口。

2. Chainlit 的底层设计哲学与核心组件解构

Chainlit 的代码量不大,但它的架构设计非常克制且精准。理解它,关键不是记住所有装饰器名字,而是抓住它如何用“事件驱动 + 配置即代码”来解耦前后端心智负担。整个框架围绕三个核心契约展开:会话生命周期契约、UI 交互契约、配置契约。这三者共同构成了它“零前端”的底气。

2.1 会话生命周期:不是简单的“开始-结束”,而是状态机的显式声明

很多初学者以为@cl.on_chat_start就是“欢迎语”,@cl.on_message就是“回消息”,这太浅了。Chainlit 的生命周期钩子,本质是让你在 Python 层显式定义一个会话状态机。每个钩子对应状态机的一个明确节点,而框架负责确保这些节点按序触发、状态隔离、错误兜底。

  • @cl.on_chat_start:这是会话的“构造函数”。它不仅运行一次,更关键的是,它为你创建了一个独立的异步上下文。在这个函数里初始化的变量(比如llm = Ollama(model="phi3")),会绑定到当前会话的整个生命周期。不同用户打开两个标签页,会启动两个完全隔离的on_chat_start实例,互不干扰。我常在这里加载模型、读取配置文件、初始化数据库连接池——因为这里保证了“一次初始化,全程可用”,避免了每次on_message都重复加载模型的性能灾难。

  • @cl.on_message:这是状态机的“主循环入口”。但它不是被动等待,而是主动参与流控。当你await cl.Message(...).send()时,Chainlit 不仅把消息推到前端,还会自动锁定当前会话的输入框,防止用户在响应未完成时狂点发送。更关键的是,它天然支持async for chunk in llm.astream()这种异步生成器,框架会帮你把每个chunk包装成独立的Message对象,并处理好前端的增量渲染、光标位置、中断信号传递。这背后是它对 asyncio 事件循环的深度集成,而不是简单套个asyncio.to_thread

  • @cl.on_stop:这是最容易被忽略的“安全阀”。当用户点击 ⏹ 按钮,它触发的不是简单的cancel(),而是向当前正在执行的on_messageaction_callback函数抛出asyncio.CancelledError。这意味着你可以在try/except中优雅清理资源:关闭数据库事务、释放 GPU 显存、取消正在运行的 LangChain Agent 的子任务。我在线上环境遇到过用户上传 2GB 日志文件后中途关闭页面,若没on_stop处理,那个asyncio.sleep(300)会一直占着线程。现在,on_stop里加一行if hasattr(llm, 'cancel'): llm.cancel()就能立刻释放。

  • @cl.on_chat_end:这是会话的“析构函数”。它触发时机很严格:用户关闭标签页、刷新页面、或主动点击“新建会话”。注意,它不等于浏览器关闭事件,而是 Chainlit 服务端检测到会话心跳超时(默认 5 分钟)后的主动清理。我用它来做三件事:1)将本次会话的完整日志(含用户提问、模型响应、耗时、token 数)写入本地 SQLite;2)如果启用了持久化(persistence.enabled = true),则调用cl.save_state()保存关键变量;3)发送 Slack 通知:“用户 A 的会话已结束,平均响应时间 1.2s”。这比前端监听页面卸载事件可靠得多,因为后者在用户强制 kill 进程时根本不会触发。

提示:@cl.on_chat_resumeon_chat_end的镜像,只在启用持久化后生效。当用户带着旧会话 ID 回来,它会在on_chat_start之前运行,让你有机会从存储中恢复state。我习惯在这里加载上次的对话历史到cl.MessageHistory,让用户感觉“无缝续聊”,而不是冷冰冰的“欢迎回来”。

2.2 UI 交互契约:按钮、文件、滑块——不是组件,而是“动作声明”

Chainlit 的 UI 元素,本质上都是对“用户意图”的结构化声明。cl.Action不是一个按钮对象,而是一份动作协议:它告诉框架“当用户执行这个操作时,请调用名为 X 的 Python 函数,并附带 payload 数据”。这种设计彻底规避了前端状态管理的复杂性。

  • cl.Actionpayload字段是精髓。它不限于字符串,可以是任意 JSON-serializable 结构。比如,我做过一个法律合同审查工具,按钮是“高亮风险条款”,payload{"section": "liability", "severity": "high"}@cl.action_callback("highlight")收到后,直接解包就能调用对应的规则引擎,无需在前端用 JS 维护一堆>python --version # 必须 >= 3.8

    我用的是 Python 3.11.9,虚拟环境是venv(不推荐 conda,Chainlit 对 conda 的路径处理偶有 bug):

    python -m venv .venv source .venv/bin/activate # Linux/macOS # .venv\Scripts\activate # Windows pip install --upgrade pip pip install chainlit

    注意:不要装langchain!这个项目纯静态,装了反而增加启动时间。Chainlit 本身不依赖 LangChain,只有后续 Ollama 项目才需要。

    代码实现:main.py
    import chainlit as cl import random import time # 预定义内容池——这里放的是真实项目中积累的“用户激励语料” FUN_FACTS = [ "💡 Did you know? Chainlit supports file uploads and custom themes!", "💡 You can add buttons, sliders, and images directly in your chatbot UI!", "💡 Chainlit supports real-time tool execution with LangChain and LLMs!", "💡 You can customize the look of your chatbot with just a CSS file!", "💡 Chainlit lets you connect to tools using Model Context Protocol (MCP)!" ] SUPRISES = [ "🎉 Surprise! You're doing great!", "🚀 Keep it up, you're making awesome progress!", "🌟 Fun fact: Someone out there just smiled because of you. Why not make it two?", "👏 Bravo! You just unlocked +10 imaginary developer XP!", "💪 Remember: Even bugs fear your debugging skills!" ] # 关键技巧:用字典缓存按钮,避免每次创建新对象 BUTTON_CACHE = {} @cl.on_chat_start async def on_chat_start(): """会话启动:初始化按钮并发送欢迎消息""" # 创建按钮列表——注意:cl.Action 的 name 必须唯一且小写 actions = [ cl.Action( name="surprise_button", label="🎁 Surprise Me", icon="gift", # payload 可以是任意 dict,这里存类型标识 payload={"type": "surprise"} ), cl.Action( name="fact_button", label="💡 Did You Know?", icon="lightbulb", payload={"type": "fact"} ) ] # 发送初始消息,附带按钮 await cl.Message( content="Hello! I'm your friendly Chainlit assistant. 🌟\n\nWhat would you like to explore today?", actions=actions ).send() # 实测心得:这里加个日志,方便调试会话生命周期 print(f"[INFO] Chat session started at {time.strftime('%H:%M:%S')}") @cl.on_message async def on_message(message: cl.Message): """消息处理:这里处理用户手动输入(虽然本项目不用,但留着备用)""" # 如果用户真的发了文字,我们礼貌回复 if message.content.strip(): await cl.Message( content=f"👋 Thanks for your message: '{message.content[:20]}...'. \nTry clicking the buttons above instead!" ).send() @cl.action_callback("surprise_button") async def on_surprise(action: cl.Action): """惊喜按钮回调:随机选一条激励语""" # 从缓存中获取按钮(可选优化) if "surprise_button" not in BUTTON_CACHE: BUTTON_CACHE["surprise_button"] = action # 模拟一点“思考”延迟,让 UI 更真实 await cl.Message(content="✨ Generating your surprise...").send() await cl.sleep(0.8) # 非阻塞等待 # 随机选择并发送 surprise = random.choice(SUPRISES) await cl.Message(content=surprise).send() # 重新发送按钮,保持 UI 一致性 await _send_action_buttons() @cl.action_callback("fact_button") async def on_fact(action: cl.Action): """知识按钮回调:随机选一条 Chainlit 小贴士""" await cl.Message(content="🔍 Fetching a fun fact...").send() await cl.sleep(0.6) fact = random.choice(FUN_FACTS) await cl.Message(content=fact).send() await _send_action_buttons() # 辅助函数:统一发送按钮,避免重复代码 async def _send_action_buttons(): """重新发送按钮组,维持交互循环""" actions = [ cl.Action(name="surprise_button", label="🎁 Surprise Me", icon="gift", payload={"type": "surprise"}), cl.Action(name="fact_button", label="💡 Did You Know?", icon="lightbulb", payload={"type": "fact"}) ] await cl.Message(content="What's next?", actions=actions).send()
    运行与调试

    在项目根目录执行:

    chainlit run main.py -w # -w 表示热重载,改代码自动刷新

    浏览器打开http://localhost:8000,你会看到:

    1. 初始界面:欢迎消息 + 两个彩色按钮,图标清晰。
    2. 点击“Surprise Me”:先显示“✨ Generating...”,0.8秒后出现随机激励语,下方自动恢复两个按钮。
    3. 点击“Did You Know?”:同理,显示小贴士。
    4. 刷新页面:会话重置,重新走on_chat_start,按钮重建。

    实测避坑:

    • 按钮不显示?检查name是否含空格或大写字母(必须全小写、下划线)。Chainlit 对name敏感,"Surprise Button"会失败。
    • 点击无反应?确保@cl.action_callback("xxx")的字符串和cl.Action(name="xxx")完全一致,包括大小写。
    • 消息乱序?Chainlit 默认按时间戳排序。如果await cl.Message().send()调用顺序错乱,UI 会错位。永远先await思考消息,再await内容消息。
    • 热重载失效?删除.chainlit文件夹,重启。Chainlit 的缓存有时会卡住。

    这个项目虽小,但已覆盖 Chainlit 80% 的核心能力:生命周期钩子、UI 动作、消息流控、状态管理。它证明了一件事:Chainlit 的“零前端”不是口号,是能跑通真实交互闭环的工程现实

    3.2 项目二:Ollama 驱动的动态“Surprise Me”机器人

    现在升级。目标:用本地运行的 Ollama 模型(Mistral)实时生成惊喜语和知识贴士,同时保留静态版的所有 UI 交互。这考验 Chainlit 与 LLM 生态的集成深度。

    环境准备:Ollama 安装与模型拉取

    Ollama 是跨平台的,官网下载安装包即可(https://ollama.com/download)。安装后验证:

    ollama --version # 应输出类似 ollama version 0.3.10

    拉取 Mistral 模型(轻量、快、适合本地):

    ollama pull mistral # 可选:拉取更小的 phi3 模型(仅2.3GB) # ollama pull phi3

    启动 Ollama 服务(后台运行):

    ollama serve # 此命令会阻塞终端,建议新开一个终端窗口运行
    依赖安装

    回到 Chainlit 项目目录,激活虚拟环境:

    pip install langchain langchain-community

    注意:langchain-community是必须的,它包含了 Ollama 的集成模块。langchain本身是核心。

    代码实现:main_ollama.py
    import chainlit as cl from langchain_community.llms import Ollama import random import time # 全局模型实例——在 on_chat_start 中初始化,避免每次请求都重建 llm = None # 提示词模板:精心设计,控制输出长度和风格 SURPRISE_PROMPT_TEMPLATE = """You are a cheerful, encouraging AI assistant for developers. Generate ONE short, uplifting, and fun message (max 15 words) that motivates a developer. Use emojis sparingly (max 1). Do NOT include any explanations or markdown. Just output the message. Example output: 🎉 You're crushing it today!""" FACT_PROMPT_TEMPLATE = """You are a helpful, concise AI assistant for AI developers. Generate ONE fun, accurate, and practical fact about LLMs, RAG, or Chainlit framework. Keep it under 20 words. No explanations, no markdown, no quotes. Just output the fact. Example output: 💡 Chainlit's config.toml lets you enable file uploads without touching Python code.""" @cl.on_chat_start async def on_chat_start(): """会话启动:初始化模型并发送欢迎消息""" global llm try: # 初始化 Ollama 模型——指定模型名、温度、超时 llm = Ollama( model="mistral", # 确保此模型已通过 ollama pull 下载 temperature=0.7, timeout=120, # 重要:设置超时,防止模型卡死 num_predict=128 # 限制最大生成 token 数,防失控 ) # 测试模型连通性(可选,但强烈推荐) test_response = llm.invoke("Say 'Model ready' in one word.") print(f"[DEBUG] Model test: {test_response.strip()}") await cl.Message( content="🤖 Hello! I'm powered by Mistral running locally via Ollama.\n\nClick a button below to get a dynamic surprise or fact!" ).send() except Exception as e: # 模型初始化失败的降级处理 error_msg = f"⚠️ Model init failed: {str(e)[:50]}..." print(f"[ERROR] {error_msg}") await cl.Message(content=error_msg).send() # 降级到静态版逻辑 await _send_static_buttons() @cl.on_message async def on_message(message: cl.Message): """消息处理:备用通道,处理用户手动输入""" if message.content.strip(): await cl.Message( content="💬 I'm optimized for button interactions! Try clicking 'Surprise Me' or 'Did You Know?' above." ).send() @cl.action_callback("surprise_button") async def on_surprise(action: cl.Action): """惊喜按钮:调用 LLM 生成激励语""" if not llm: await _handle_llm_failure("surprise") return await cl.Message(content="🤔 Thinking of something uplifting...").send() try: # 构建完整提示词 full_prompt = SURPRISE_PROMPT_TEMPLATE # 流式调用——这才是 Chainlit 的精华 response = "" async for chunk in llm.astream(full_prompt): if isinstance(chunk, str): response += chunk # 实时流式发送,但只在 chunk 非空时 if chunk.strip(): await cl.Message(content=chunk, author="LLM", stream=True).send() # 清理响应:去除多余空格和换行 cleaned = response.strip() if not cleaned: cleaned = "🎉 Keep coding! You're amazing!" # 发送最终消息(覆盖流式消息) await cl.Message(content=cleaned, author="Assistant").send() except Exception as e: await _handle_llm_failure("surprise", str(e)) finally: # 无论成功失败,都恢复按钮 await _send_action_buttons() @cl.action_callback("fact_button") async def on_fact(action: cl.Action): """知识按钮:调用 LLM 生成小贴士""" if not llm: await _handle_llm_failure("fact") return await cl.Message(content="📚 Researching a cool fact...").send() try: full_prompt = FACT_PROMPT_TEMPLATE response = "" async for chunk in llm.astream(full_prompt): if isinstance(chunk, str): response += chunk if chunk.strip(): await cl.Message(content=chunk, author="LLM", stream=True).send() cleaned = response.strip() if not cleaned: cleaned = "💡 Chainlit makes prototyping LLM apps feel like magic." await cl.Message(content=cleaned, author="Assistant").send() except Exception as e: await _handle_llm_failure("fact", str(e)) finally: await _send_action_buttons() # 降级处理函数 async def _handle_llm_failure(action_type: str, error: str = ""): """当 LLM 调用失败时,提供友好降级""" fallbacks = { "surprise": [ "🎉 Surprise! You're doing great!", "🚀 Keep it up, you're making awesome progress!" ], "fact": [ "💡 Did you know? Chainlit supports file uploads!", "💡 You can add buttons with cl.Action in seconds!" ] } msg = random.choice(fallbacks.get(action_type, fallbacks["surprise"])) error_note = f" (Fallback: {error[:30]}...)" if error else "" await cl.Message(content=f"{msg}{error_note}").send() # 通用按钮发送函数 async def _send_action_buttons(): """发送标准按钮组""" actions = [ cl.Action(name="surprise_button", label="🎁 Surprise Me", icon="gift", payload={"type": "surprise"}), cl.Action(name="fact_button", label="💡 Did You Know?", icon="lightbulb", payload={"type": "fact"}) ] await cl.Message(content="✨ What would you like next?", actions=actions).send() # 静态降级按钮(当模型未就绪时) async def _send_static_buttons(): """纯静态按钮,用于模型初始化失败时""" actions = [ cl.Action(name="surprise_button", label="🎁 Surprise Me (Static)", icon="gift"), cl.Action(name="fact_button", label="💡 Did You Know? (Static)", icon="lightbulb") ] await cl.Message(content="⚠️ Local model not ready. Using static responses.").send() await cl.Message(content="Try again in a moment!", actions=actions).send()
    运行与性能调优
    1. 确保 Ollama 服务运行:在终端 A 执行ollama serve
    2. 启动 Chainlit:在终端 B,进入项目目录,执行:
      chainlit run main_ollama.py -w
    3. 首次访问:会看到“Model ready”测试消息,然后进入主界面。

    实测性能数据(M1 Mac Mini, 16GB RAM):

    • Mistral 首次响应:1.8~2.5 秒(含模型加载)
    • 后续响应:0.9~1.4 秒(模型已驻留内存)
    • 流式响应:首 token 延迟 0.3~0.5 秒,之后每 0.1~0.2 秒一个 chunk

    关键调优参数:

    • timeout=120:防止 Ollama 偶尔卡死导致整个会话挂起。
    • num_predict=128:硬性限制生成长度,避免模型“自由发挥”输出长篇大论。
    • temperature=0.7:平衡创意性和稳定性,0.3 太死板,0.9 太跳跃。

    实测避坑:

    • Ollama 连接拒绝?检查ollama serve是否在运行,且端口11434未被占用。Chainlit 默认连http://localhost:11434
    • 流式响应卡住?astream循环中加print(f"Chunk: {repr(chunk)}"),确认 Ollama 是否真在流式输出。有些模型(如llama3)默认不流式,需加--stream参数启动。
    • 中文乱码?Mistral 原生支持中文,但若用phi3,需在提示词开头加"Answer in Chinese:"
    • 内存暴涨?Ollama实例是全局的,但astream会创建新协程。确保on_chat_start只初始化一次,不要在on_message里反复Ollama(...)

    这个项目展示了 Chainlit 的真正威力:它把复杂的 LLM 集成,简化为几行 Python 装饰器和一个配置项。你不需要懂 WebSocket、不需要写前端 JS、不需要处理 CORS,只需要关注“用户想做什么”和“模型该怎么答”。

    4. Chainlit 高阶配置与生产级实践指南

    学到这里,你已经能做出可用的原型。但要走向生产,还需要跨越几个关键门槛:配置管理、错误防御、性能监控、安全加固。这些不是“锦上添花”,而是决定项目能否存活的“生存技能”。

    4.1config.toml深度解析:超越文档的实战配置

    Chainlit 的官方文档只列出了配置项,但没告诉你在什么场景下必须开、什么场景下必须关、开错了会怎样。以下是我在 17 个项目中沉淀的配置黄金法则。

    创建配置文件

    在项目根目录创建.chainlit/config.toml(注意是.chainlit文件夹,不是项目根目录):

    # .chainlit/config.toml [app] # 应用名称,显示在浏览器标签页 name = "Chainlit Demo" # 应用描述,显示在登录页 description = "A demo of Chainlit capabilities" [UI] # 助手名称,影响所有 Message 的 author 默认值 name = "AI Assistant" # 链式思维渲染模式:full=展开所有步骤,compact=只显示最终答案 cot = "full" # 主题:light/dark/system theme = "system" # 自定义 CSS 文件路径(相对于 .chainlit 目录) custom_css = "custom.css" [persistence] # 启用会话持久化——线上环境必开! enabled = true # 持久化后端:sqlite(默认)/redis/mongodb backend = "sqlite" # SQLite 文件路径 db_path = "../chat_history.db" [features.spontaneous_file_upload] # 启用文件上传——但必须严格限制! enabled = true # 允许的 MIME 类型——宁缺毋滥! accept = ["image/jpeg", "image/png", "application/pdf", "text/plain"] # 最大文件数 max_files = 3 # 单文件最大尺寸(MB)——500MB 对服务器是灾难 max_size_mb = 25 # 上传超时(秒) timeout = 300 [features] # 用户消息自动滚动到最新——UX 基础项 user_message_autoscroll = true # 允许用户编辑自己的消息——RAG 场景神器 edit_message = true # 启用消息复制按钮——用户常需复制 prompt copy_message = true # 启用消息引用(回复某条消息)——提升对话深度 reply = true [telemetry] # 禁用遥测——生产环境必须关! enabled = false [run] # 开发模式端口 port = 8000 # 是否允许远程访问(生产环境设为 false) dev = true
    关键配置项实战解读
    • [persistence] enabled = true线上环境的生死线。没有它,用户刷新页面就丢失所有上下文,体验极差。但开启后,你必须处理on_chat_resume。我的标准做法是:

      @cl.on_chat_resume async def on_chat_resume(thread: cl.ThreadDict): # 从 thread['metadata'] 中恢复关键状态 user_id = thread.get("metadata", {}).get("user_id") if user_id: # 加载该用户的个性化设置 settings = load_user_settings(user_id) cl.user_session.set("settings", settings)
    • [features.spontaneous_file_upload] accept安全第一原则。永远不要用"*/*"!我曾见一个项目因开放所有类型,被上传了.exe文件,虽然后端不执行,但占满磁盘。accept = ["image/*", "application/pdf"]是安全底线,再根据业务加白名单。

    • [features] edit_message = trueRAG 场景的隐藏王牌。用户提问“帮我总结这份PDF”,然后发现漏了关键词,双击编辑成“帮我总结这份PDF,重点关注第三章”。Chainlit 会自动把新消息作为on_messagemsg传入,你无需任何额外代码就能处理。这比让用户删掉重发友好十倍。

    • [telemetry] enabled = false合规硬性要求。Chainlit 默认收集匿名使用数据,但企业内网或 GDPR 环境下必须关闭。关掉后,启动日志里不再有Telemetry enabled提示。

    提示:配置文件支持环境变量插值。例如db_path = "${CHAINLIT_DB_PATH:-../chat_history.db}",便于 Docker 部署时注入。

    4.2 错误防御体系:构建坚不可摧的用户体验

    Chainlit 的优雅在于它把错误处理也变成了装饰器。但默认行为(如 500 错误页)对用户不友好。你需要三层防御:

    第一层:Python 异常捕获(在回调函数内)

    这是最细粒度的防御。每个@cl.action_callback@cl.on_message都应包裹try/except

    @cl.action_callback("surprise_button") async def on_surprise(action: cl.Action): try: # 你的核心逻辑 result = await generate_surprise() await cl.Message(content=result).send() except ModelTimeoutError: await cl.Message(content="⏳ Model is taking too long. Try again!").send() except ValidationError
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/16 8:34:03

零代码本地部署AI智能体:Dify+Ollama+Qwen2实战指南

1. 项目概述:为什么“零代码本地部署AI智能体”正在成为技术人的刚需最近三个月,我几乎每天都会收到五条以上类似的消息:“Dify本地部署失败”“Ollama拉取Qwen2卡在99%”“Dify连不上本机Ollama,报错connection refused”——不是…

作者头像 李华
网站建设 2026/6/16 8:26:00

PSIVG框架:物理模拟如何提升视频生成的真实感

1. PSIVG框架概述:物理模拟如何重塑视频生成范式在游戏开发和影视特效领域,我们经常遇到一个核心痛点:AI生成的物体运动看起来"不对劲"。一个杯子从桌上掉落时像羽毛般飘落,台球碰撞后违反动量守恒,这些违背…

作者头像 李华
网站建设 2026/6/16 8:26:00

万用表使用指南:从核心功能到安全操作与故障排查

1. 万用表:从入门到精通的电子诊断利器如果你刚开始接触电子制作、家电维修,或者只是好奇家里的电器为什么突然不工作了,那么你迟早会需要用到一件工具——万用表。它不像螺丝刀、钳子那样直观,但对于任何涉及电的领域&#xff0c…

作者头像 李华
网站建设 2026/6/16 8:25:02

.NET Web开发路线图:从WebForms到Minimal API的演进与实战

1. 这不是教程,是十年踩坑后画的一张.NET Web开发路线图我从2008年用Visual Studio 2005写第一个ASP.NET WebForms页面开始,到今天带团队落地过17个中大型Web系统——电商后台、医疗HIS接口网关、制造业MES数据看板、政务审批中台……所有项目都跑在.NET…

作者头像 李华
网站建设 2026/6/16 8:25:00

策略蒸馏实战:让小模型学会Qwen的思考方式

1. 项目概述:一场被38次点名的策略蒸馏实践,到底在解决什么问题?最近刷技术圈动态时,我注意到Thinking Machines Lab博客里一篇题为《Policy Distillation in Practice: Lessons from Scaling Qwen》的文章突然被大量转发。标题本…

作者头像 李华