Mercury能把Jupyter Notebook一键变成可交互的Web应用,让数据分析和RAG原型快速分享给团队用,不用写前端代码。这篇文章带你了解Mercury怎么把Notebook转成前端应用、怎么加聊天组件和交互控件、部署怎么做、适合什么场景,适合想把分析成果产品化的数据同学和前端开发者参考。

岗位关联

需要学习Mercury Notebook发布,AI交互前端(WebGPU/浏览器推理)是下一代用户体验的关键。本篇涵盖React/Vue、WebGPU、AI组件、浏览器推理,帮助你构建AI交互前端。

目录
  1. 可视化演示
  2. Mercury工作原理与Notebook转Web机制
  3. Mercury交互组件与Widget配置
  4. 聊天组件集成与RAG前端实现
  5. Mercury部署与多用户管理
  6. Mercury在RAG实践中的应用场景
  7. AI 应用场景
  8. Python 演示
  9. 常见问答
  10. 常见误区
  11. 参考来源

可视化演示

展示Mercury将Jupyter Notebook转换为交互Web应用的流程、RAG聊天组件集成及Docker部署架构,交互切换三种模式。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+CAPS.mode1+'

Mercury工作原理与Notebook转Web机制

Mercury是一个基于Python的开源框架,允许开发者直接在Jupyter Notebook中构建交互式Web应用。其核心原理是解析Notebook中的YAML配置块,将代码单元(Markdown和Code Cell)转换为前端Web组件。与Voila不同,Mercury采用声明式配置而非代码驱动的方式定义交互组件,开发者在Notebook顶部通过YAML语法声明Widget参数,Mercury在执行Notebook前将这些参数注入为Python变量。这种架构使得数据科学家无需学习HTML/CSS/JavaScript即可创建功能完整的Web应用。

Notebook转Web的核心流程包括三个阶段:第一阶段是配置解析,Mercury读取Notebook中的YAML front matter,提取Widget定义并验证参数类型;第二阶段是Notebook执行,Mercury将YAML中定义的参数作为输入变量注入Notebook的执行环境,按顺序执行所有代码单元;第三阶段是前端渲染,Mercury将执行结果(Markdown输出、代码输出、图表等)转换为响应式Web页面,并生成对应的Widget交互界面。每次用户修改Widget值时,Mercury会重新执行整个Notebook,这是其与流式更新框架的关键区别。

# Mercury YAML配置解析示例 yaml_config = ''' query_text: type: text label: Search Query value: What is RAG top_k_slider: type: slider label: Number of Results min: 1 max: 10 value: 3 ''' # Mercury在执行前将YAML参数注入为Python变量 # query_text = 'What is RAG' # top_k_slider = 3

Mercury的架构设计体现了'Notebook-First'的理念。后端基于Django框架,负责Notebook执行和会话管理;前端使用JavaScript渲染交互界面,通过WebSocket与后端通信。Mercury支持两种运行模式:开发模式(Run mode)允许实时预览和调试,生产模式(Serve mode)提供优化后的静态Web服务。在RAG应用场景中,Mercury的优势在于能够将数据处理的完整流程(文档加载、分块、向量化、检索、生成)封装在单个Notebook中,一键转换为可分享的Web应用。

深入理解Mercury的执行模型对于构建高性能Web应用至关重要。由于每次交互都触发完整的Notebook重新执行,开发者需要优化Notebook结构:将耗时操作(如模型加载、数据预处理)放在Notebook前部并使用缓存机制,将交互相关的代码放在后部。Mercury还提供了show和hide指令来控制代码和输出的显示,使得最终用户可以专注于交互界面而无需看到底层代码。这种'白盒开发、黑盒使用'的模式非常适合RAG原型的快速交付和团队内部测试。

Mercury交互组件与Widget配置

Mercury的Widget系统是其区别于其他Notebook转Web工具的核心特性。与Voila依赖ipywidgets在代码中创建交互组件不同,Mercury采用YAML声明式配置,所有Widget参数在Notebook执行前就已确定。支持的Widget类型包括:text(文本输入)、textarea(多行文本)、slider(滑块)、select(下拉选择)、checkbox(复选框)、file_upload(文件上传)和numeric(数值输入)。每种Widget类型都有特定的参数集,如slider需要min、max、step参数,select需要choices列表。

# Widget类型与参数配置 class MercuryYAMLParser: def parse_slider_widget(self, params): return { 'type': 'slider', 'label': params.get('label', 'Slider'), 'min': params.get('min', 0), 'max': params.get('max', 100), 'step': params.get('step', 1), 'value': params.get('value', 50), }

Widget的状态管理机制是理解Mercury交互行为的关键。当用户在前端修改Widget值时,Mercury不会像传统Web应用那样通过AJAX请求局部更新页面,而是触发整个Notebook的重新执行。新的Widget值会作为Python变量注入Notebook的执行环境,所有代码单元按顺序重新运行。这意味着开发者不需要编写事件处理代码,Notebook中的代码自然就是Widget变化的'响应函数'。但这种机制也带来了性能挑战:对于包含大量计算或长时间运行代码的Notebook,每次交互的延迟可能很显著。

在RAG应用中,Widget配置直接影响用户体验和系统功能。典型的RAG应用Widget配置包括:文本输入框用于用户查询、滑块用于控制检索结果数量(top_k)、下拉选择用于切换LLM模型、文件上传组件用于文档导入、复选框用于开关缓存或流式输出等功能。合理设计Widget的参数范围和默认值对应用可用性至关重要。例如,top_k滑块的max值不应超过向量数据库的实际容量,文件上传的max_size参数需要与后端处理能力匹配。Mercury的Widget系统虽然简洁,但通过合理的参数设计可以覆盖大多数RAG应用的交互需求。

Widget验证和错误处理是生产环境中不可忽视的方面。Mercury在解析YAML配置时会进行类型检查和范围验证,但开发者仍需注意边界情况:slider的min值不能大于max值、select的choices不能为空、file_upload的accept参数需要使用正确的MIME类型或文件扩展名格式。在实际部署中,建议为每个Widget设置合理的默认值,并在Notebook代码中添加对Widget值的防御性检查,确保即使输入异常也不会导致Notebook执行失败。

聊天组件集成与RAG前端实现

Mercury提供了内置的聊天组件(Chat Component),用于在Notebook Web应用中实现对话式交互界面。该组件通过YAML配置启用,支持用户消息输入、助手消息展示和消息历史记录。在RAG场景中,聊天组件充当用户与检索增强生成系统之间的交互桥梁:用户通过聊天界面输入问题,Mercury将问题传递给Notebook中定义的RAG处理逻辑,检索相关文档片段,调用LLM生成回答,并将结果通过聊天组件展示给用户,同时附带引用来源。

# RAG前端聊天组件消息流 class MercuryChatComponent: def send_message(self, user_input, context=None): user_msg = ChatMessage('user', user_input) self.messages.append(user_msg) response_content = self._generate_response(user_input, context) assistant_msg = ChatMessage('assistant', response_content, {'sources': context.get('sources', []) if context else []}) self.messages.append(assistant_msg) return {'user_message': user_msg.to_dict(), 'assistant_message': assistant_msg.to_dict()}

RAG前端的完整实现涉及四个核心模块:文档上传与处理模块、文档分块(Chunking)模块、向量检索模块和回答生成模块。文档上传模块通过file_upload Widget接收用户上传的PDF、TXT或Markdown文件;分块模块将长文档按固定大小(通常200-500字符)和重叠窗口(通常50字符)切分为可管理的片段;向量检索模块将用户查询和文档片段转换为向量表示,通过余弦相似度计算找到最相关的片段;回答生成模块将检索到的上下文与用户问题组合,调用LLM API生成最终回答。

在Mercury中实现RAG前端的关键挑战在于状态管理。由于Mercury每次交互都重新执行Notebook,文档向量索引等中间状态需要在每次执行时重建或从持久化存储加载。实践中常用的优化策略包括:将文档索引缓存在磁盘上(使用pickle或专用向量数据库),仅在文档内容变化时重建索引;将模型加载放在Notebook开头并利用Mercury的执行缓存;使用全局变量或文件系统存储对话历史,避免每次重新执行时丢失上下文。这些优化对于包含大量文档或复杂模型的RAG应用尤为重要。

聊天组件的消息流设计需要考虑用户体验的多个方面。首先是消息格式:除了纯文本外,Mercury的聊天组件支持Markdown格式的回答,可以展示代码块、列表和链接。其次是引用来源的展示:RAG系统返回的回答应附带来源引用,帮助用户验证信息的准确性。第三是流式输出:虽然Mercury本身不支持真正的流式传输(因为每次交互都重新执行Notebook),但可以通过在Notebook中模拟流式效果(逐字符打印)来改善用户体验。最后是错误处理:当LLM API不可用或检索结果为空时,聊天组件应优雅地降级,提供有意义的错误提示而非显示空白或崩溃。

Mercury部署与多用户管理

Mercury的部署架构基于Docker容器化技术,提供标准化的应用打包和运行环境。生产部署的核心组件包括:Mercury Server(基于Django)负责Notebook执行和HTTP请求处理、Notebook Kernel(基于Jupyter Kernel Gateway)负责Python代码的隔离执行、以及可选的Nginx反向代理用于负载均衡和SSL终止。Docker镜像mljar/mercury:latest包含了运行Mercury应用所需的全部依赖,开发者只需将Notebook文件挂载到容器中即可启动服务。

# 部署配置与多用户会话管理 class DeploymentManager: def configure_deployment(self, config): self.deploy_config = { 'app_name': config.get('app_name', 'mercury-app'), 'notebook_path': config.get('notebook_path', 'app.ipynb'), 'port': config.get('port', 5006), 'workers': config.get('workers', 4), 'docker_image': config.get('docker_image', 'mljar/mercury:latest'), } def create_session(self, user_id): session_id = hashlib.md5(f'{user_id}:{len(self.sessions)}'.encode()).hexdigest()[:12] session = KernelSession(session_id, user_id) self.sessions[session_id] = session return {'session_id': session_id, 'status': 'created'}

Kernel隔离是Mercury多用户支持的核心机制。每个用户会话(或每个用户,取决于配置)分配独立的Jupyter Kernel,确保不同用户的变量空间、执行状态和内存完全隔离。这种设计防止了一个用户的操作影响其他用户,但也带来了资源消耗的挑战:每个Kernel都是一个独立的Python进程,占用独立的内存空间。在RAG应用中,如果每个Kernel都需要加载向量索引和LLM模型,内存消耗会迅速增长。解决方案包括:使用共享内存映射(Shared Memory Mapping)存储只读的向量索引,使用模型服务(Model Serving)替代每个Kernel内嵌模型,以及合理设置Kernel的内存限制和超时回收策略。

多用户管理涉及会话生命周期控制、资源配额和权限管理三个维度。会话生命周期包括创建、活跃、空闲和终止四个状态,Mercury的部署管理器负责监控会话状态并在超时时自动回收Kernel资源。资源配额限制每个用户可创建的会话数量和每个会话可使用的内存上限,防止单个用户耗尽系统资源。权限管理通过enable_auth配置启用用户认证,支持基本的用户名密码认证和SSO集成。在RAG应用场景中,还需要考虑文档级别的访问控制:不同用户可能有权访问不同的文档集合,这需要在检索阶段实施过滤策略。

生产环境的Mercury部署需要考虑高可用性和性能优化。水平扩展方面,可以通过部署多个Mercury Server实例配合负载均衡器实现;垂直扩展方面,可以调整workers参数增加单实例的并发处理能力。监控方面,需要关注Kernel内存使用率、Notebook执行时间、会话并发数和API响应延迟等指标。对于RAG应用,还需要特别监控向量检索的延迟和LLM API的调用频率,这些往往是性能瓶颈所在。合理的缓存策略(如缓存常见查询的检索结果)可以显著提升多用户场景下的响应速度。

Mercury在RAG实践中的应用场景

Mercury在RAG实践中最典型的应用场景是快速原型验证。数据团队可以在单个Notebook中完成从文档处理到问答生成的完整RAG流程,然后通过Mercury一键转换为Web应用,供产品经理和业务方直接在浏览器中测试。这种模式将原型验证周期从传统的数周缩短到数天,极大加速了RAG方案的迭代速度。在实际项目中,团队通常会维护多个Notebook版本,分别测试不同的分块策略、检索方法和Prompt模板,通过Mercury部署为并行的Web应用进行A/B测试。

另一个重要场景是内部知识库问答系统的快速搭建。企业可以将内部文档(技术文档、操作手册、FAQ等)通过Mercury应用暴露给员工查询。Notebook中实现文档加载、分块和向量化逻辑,通过file_upload Widget允许管理员上传新文档,通过聊天组件提供查询界面。这种方案的优势在于部署简单(单个Docker容器)、维护成本低(所有逻辑在Notebook中)且易于更新(修改Notebook即可改变应用行为)。适合中小规模(文档量在数千以内)的知识库场景。

# RAG管道完整流程 class RAGPipeline: def upload_document(self, doc_id, content, metadata=None): chunks = self.chunker.chunk_document(content, doc_id) added = self.vector_store.add_chunks(chunks) return {'doc_id': doc_id, 'chunks_created': added, 'status': 'indexed'} def query(self, question, top_k=3): retrieved = self.vector_store.search(question, top_k=top_k) answer = self._generate_answer(question, '', retrieved) return {'question': question, 'answer': answer, 'sources': [{'chunk_id': r['chunk_id'], 'score': r['similarity_score']} for r in retrieved]}

Mercury在RAG教学与演示场景中也有独特价值。教育者可以将RAG的每个步骤(文档分块、向量编码、相似度计算、上下文组装、回答生成)封装在独立的Notebook单元中,通过Mercury的Widget系统让学习者调整参数(如chunk_size、top_k、温度参数)并实时观察对结果的影响。这种交互式教学方式比静态文档或视频更直观,学习者可以通过动手实验深入理解RAG各环节的工作原理和参数影响。

Mercury在RAG实践中的局限性也值得深入分析。首先是性能限制:由于每次交互都重新执行Notebook,对于大规模文档集合(数万文档以上)的RAG应用,检索延迟会很高。其次是状态管理困难:对话历史、缓存状态等需要在每次执行时恢复,增加了实现复杂度。第三是并发能力有限:Mercury的设计定位是原型工具而非生产级服务,在高并发场景下(数百用户同时使用)会遇到Kernel资源瓶颈。因此在实际项目中,Mercury更适合用于RAG原型验证和内部工具,而非面向大量外部用户的生产系统。当原型验证通过后,通常需要将核心逻辑迁移到专门的RAG框架(如LangServe、FastAPI)进行生产化部署。

AI 应用场景

Mercury RAG原型快速交付应用

阶段:模型部署

解决的问题:解决RAG原型难以快速交付为可交互Web应用供团队测试的问题

具体案例:数据团队用Mercury将RAG原型Notebook转为交互Web应用产品经理通过浏览器上传文档测试检索效果查看生成回答和引用来源无需开发前端页面原型验证周期从3周缩短到3天快速迭代RAG方案

企业内部知识库问答系统搭建

阶段:应用集成

解决的问题:解决企业内部知识库缺乏交互式查询界面导致信息检索效率低下的问题

具体案例:运维团队使用Mercury部署内部技术文档问答系统员工通过Web界面上传新文档并通过聊天组件查询技术问题系统基于RAG检索相关文档片段调用LLM生成回答并展示引用来源整个系统通过单个Docker容器部署维护成本极低

Python 演示

python960 行
"""Mercury Notebook-to-Web Conversion and RAG Frontend Simulation==============================================================Simulates Mercury framework's core functionality:- YAML config parsing for widgets- Notebook cell to web component conversion- Interactive widget system (file upload, text input, slider, select)- Chat component with message flow- RAG pipeline (upload -> chunk -> retrieve -> generate -> display)- Docker deployment and kernel isolation""" import jsonimport hashlibimport mathimport refrom collections import OrderedDict try:    import numpy as np    HAS_NUMPY = Trueexcept ImportError:    HAS_NUMPY = False  # ============================================================# 1. Mercury YAML Config Parser# ============================================================class MercuryYAMLParser:    """Parse YAML-like configuration for Mercury widgets."""     def __init__(self):        self.widget_types = {            "text": self._parse_text_widget,            "textarea": self._parse_textarea_widget,            "slider": self._parse_slider_widget,            "select": self._parse_select_widget,            "checkbox": self._parse_checkbox_widget,            "file_upload": self._parse_file_upload_widget,            "numeric": self._parse_numeric_widget,        }     def parse_yaml_config(self, yaml_string):        """Parse YAML configuration string into widget definitions."""        widgets = OrderedDict()        current_widget = None        current_key = None         for line in yaml_string.strip().split("\n"):            stripped = line.strip()            if not stripped or stripped.startswith("#"):                continue             indent = len(line) - len(line.lstrip())             if indent == 0 and stripped.endswith(":"):                widget_name = stripped[:-1].strip()                current_widget = {"name": widget_name, "params": {}}                widgets[widget_name] = current_widget                current_key = None            elif indent > 0 and current_widget is not None:                if ":" in stripped:                    key, value = stripped.split(":", 1)                    key = key.strip()                    value = value.strip()                    current_widget["params"][key] = self._cast_value(value)                    current_key = key         validated = {}        for name, wdef in widgets.items():            wtype = wdef["params"].get("type", "text")            if wtype in self.widget_types:                validated[name] = self.widget_types[wtype](wdef["params"])            else:                validated[name] = self._parse_text_widget(wdef["params"])         return validated     def _cast_value(self, value):        """Cast string value to appropriate Python type."""        if value.lower() == "true":            return True        if value.lower() == "false":            return False        try:            return int(value)        except ValueError:            pass        try:            return float(value)        except ValueError:            pass        if value.startswith("[") and value.endswith("]"):            inner = value[1:-1]            return [v.strip().strip('"').strip("'") for v in inner.split(",") if v.strip()]        return value.strip('"').strip("'")     def _parse_text_widget(self, params):        return {            "type": "text",            "label": params.get("label", "Text Input"),            "value": params.get("value", ""),            "max": params.get("max", 100),        }     def _parse_textarea_widget(self, params):        return {            "type": "textarea",            "label": params.get("label", "Text Area"),            "value": params.get("value", ""),            "rows": params.get("rows", 5),        }     def _parse_slider_widget(self, params):        return {            "type": "slider",            "label": params.get("label", "Slider"),            "min": params.get("min", 0),            "max": params.get("max", 100),            "step": params.get("step", 1),            "value": params.get("value", 50),        }     def _parse_select_widget(self, params):        return {            "type": "select",            "label": params.get("label", "Select"),            "choices": params.get("choices", []),            "value": params.get("value", ""),            "multi": params.get("multi", False),        }     def _parse_checkbox_widget(self, params):        return {            "type": "checkbox",            "label": params.get("label", "Checkbox"),            "value": params.get("value", False),        }     def _parse_file_upload_widget(self, params):        return {            "type": "file_upload",            "label": params.get("label", "Upload File"),            "accept": params.get("accept", "*"),            "max_size": params.get("max_size", 10485760),        }     def _parse_numeric_widget(self, params):        return {            "type": "numeric",            "label": params.get("label", "Number"),            "value": params.get("value", 0),            "min": params.get("min", 0),            "max": params.get("max", 1000),            "step": params.get("step", 1),        }  # ============================================================# 2. Notebook-to-Web Converter# ============================================================class NotebookCell:    """Represents a single Jupyter notebook cell."""     def __init__(self, cell_type, source, metadata=None):        self.cell_type = cell_type  # "code" or "markdown"        self.source = source        self.metadata = metadata or {}        self.output = None        self.web_component = None     def to_web_component(self):        """Convert cell to a web component representation."""        if self.cell_type == "markdown":            self.web_component = {                "component": "MarkdownBlock",                "html": self._markdown_to_html(self.source),                "interactive": False,            }        elif self.cell_type == "code":            hide_code = self.metadata.get("hide_code", False)            hide_output = self.metadata.get("hide_output", False)            self.web_component = {                "component": "CodeBlock",                "code": self.source if not hide_code else None,                "output": self.output if not hide_output else None,                "interactive": True,                "cell_id": self.metadata.get("cell_id", "unknown"),            }        return self.web_component     def _markdown_to_html(self, md_text):        """Simple markdown to HTML conversion."""        html = md_text        html = re.sub(r"^### (.+)$", r"<h3>\1</h3>", html, flags=re.MULTILINE)        html = re.sub(r"^## (.+)$", r"<h2>\1</h2>", html, flags=re.MULTILINE)        html = re.sub(r"^# (.+)$", r"<h1>\1</h1>", html, flags=re.MULTILINE)        html = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", html)        html = re.sub(r"\*(.+?)\*", r"<em>\1</em>", html)        html = re.sub(r"`(.+?)`", r"<code>\1</code>", html)        return html  class NotebookToWebConverter:    """Convert a full Jupyter notebook to a Mercury web application."""     def __init__(self):        self.cells = []        self.yaml_parser = MercuryYAMLParser()        self.widgets = {}     def load_notebook(self, notebook_dict):        """Load notebook from dictionary representation."""        for cell_data in notebook_dict.get("cells", []):            cell = NotebookCell(                cell_type=cell_data.get("cell_type", "code"),                source="".join(cell_data.get("source", [])),                metadata=cell_data.get("metadata", {}),            )            self.cells.append(cell)        return len(self.cells)     def parse_widget_config(self, yaml_config):        """Parse widget configuration from YAML."""        self.widgets = self.yaml_parser.parse_yaml_config(yaml_config)        return self.widgets     def convert(self):        """Convert all cells to web components."""        components = []        for i, cell in enumerate(self.cells):            if cell.cell_type == "code":                cell.output = self._simulate_execution(cell.source)            component = cell.to_web_component()            component["order"] = i            components.append(component)         app_structure = {            "title": "Mercury Web App",            "components": components,            "widgets": self.widgets,            "total_cells": len(self.cells),            "interactive_cells": sum(1 for c in components if c.get("interactive")),        }        return app_structure     def _simulate_execution(self, code):        """Simulate code cell execution and return output."""        if "print(" in code:            match = re.search(r'print\(["\'](.+?)["\']\)', code)            if match:                return {"type": "stdout", "data": match.group(1)}        if "import" in code:            return {"type": "stdout", "data": "[Module imported successfully]"}        if "=" in code and "==" not in code:            return {"type": "stdout", "data": "[Variable assigned]"}        return {"type": "stdout", "data": "[Cell executed]"}  # ============================================================# 3. Widget System# ============================================================class WidgetManager:    """Manage interactive widgets and their state."""     def __init__(self):        self.widgets = {}        self.state_history = []     def register_widget(self, name, widget_config):        """Register a widget with its configuration."""        self.widgets[name] = {            "config": widget_config,            "current_value": widget_config.get("value", None),            "change_count": 0,        }     def update_value(self, name, new_value):        """Update widget value and record state change."""        if name not in self.widgets:            raise ValueError(f"Widget '{name}' not registered")         old_value = self.widgets[name]["current_value"]        self.widgets[name]["current_value"] = new_value        self.widgets[name]["change_count"] += 1         self.state_history.append({            "widget": name,            "old_value": old_value,            "new_value": new_value,            "timestamp": len(self.state_history),        })         return {"changed": old_value != new_value, "widget": name}     def get_value(self, name):        """Get current value of a widget."""        if name not in self.widgets:            raise ValueError(f"Widget '{name}' not registered")        return self.widgets[name]["current_value"]     def get_all_values(self):        """Get all widget values as a dictionary."""        return {name: w["current_value"] for name, w in self.widgets.items()}     def simulate_interaction(self, name, input_value):        """Simulate user interaction with a widget."""        widget = self.widgets.get(name)        if not widget:            return {"error": f"Widget '{name}' not found"}         wtype = widget["config"]["type"]        validated = self._validate_input(wtype, input_value, widget["config"])        result = self.update_value(name, validated)        result["trigger_rerun"] = True        return result     def _validate_input(self, widget_type, value, config):        """Validate and coerce input based on widget type."""        if widget_type == "slider":            val = float(value)            val = max(config.get("min", 0), min(config.get("max", 100), val))            return val        elif widget_type == "select":            choices = config.get("choices", [])            if value in choices:                return value            return choices[0] if choices else value        elif widget_type == "checkbox":            return bool(value)        elif widget_type == "file_upload":            max_size = config.get("max_size", 10485760)            if isinstance(value, dict):                if value.get("size", 0) > max_size:                    raise ValueError("File exceeds maximum size")                return value            return {"name": str(value), "size": 0}        return value  # ============================================================# 4. Chat Component# ============================================================class ChatMessage:    """Represents a single chat message."""     def __init__(self, role, content, metadata=None):        self.role = role  # "user", "assistant", "system"        self.content = content        self.metadata = metadata or {}        self.timestamp = hashlib.md5(f"{role}:{content}".encode()).hexdigest()[:8]     def to_dict(self):        return {            "role": self.role,            "content": self.content,            "metadata": self.metadata,            "msg_id": self.timestamp,        }  class MercuryChatComponent:    """Simulate Mercury chat component with message flow processing."""     def __init__(self, system_prompt=None):        self.messages = []        self.system_prompt = system_prompt or "You are a helpful assistant."        self.is_processing = False        self.message_handlers = []     def add_system_message(self):        """Initialize chat with system message."""        sys_msg = ChatMessage("system", self.system_prompt)        self.messages.append(sys_msg)        return sys_msg.to_dict()     def send_message(self, user_input, context=None):        """Process user message and generate response."""        user_msg = ChatMessage("user", user_input, {"context": context})        self.messages.append(user_msg)         self.is_processing = True        response_content = self._generate_response(user_input, context)        self.is_processing = False         assistant_msg = ChatMessage(            "assistant",            response_content,            {"sources": context.get("sources", []) if context else []},        )        self.messages.append(assistant_msg)         return {            "user_message": user_msg.to_dict(),            "assistant_message": assistant_msg.to_dict(),            "total_messages": len(self.messages),        }     def _generate_response(self, user_input, context):        """Generate simulated response based on input and context."""        if context and "documents" in context:            relevant = self._retrieve_relevant(user_input, context["documents"])            if relevant:                summary = " ".join([d["content"][:80] for d in relevant[:2]])                return f"Based on the retrieved documents: {summary}..."         if "hello" in user_input.lower() or "hi" in user_input.lower():            return "Hello! I am a Mercury chat assistant. How can I help you?"         return f"I received your message: '{user_input}'. This is a simulated response from the Mercury chat component."     def _retrieve_relevant(self, query, documents):        """Simple keyword-based retrieval simulation."""        query_terms = set(query.lower().split())        scored = []        for doc in documents:            doc_terms = set(doc["content"].lower().split())            overlap = len(query_terms & doc_terms)            if overlap > 0:                scored.append((overlap, doc))        scored.sort(key=lambda x: x[0], reverse=True)        return [doc for _, doc in scored[:3]]     def get_history(self):        """Return full message history."""        return [msg.to_dict() for msg in self.messages]     def register_handler(self, event_type, handler):        """Register a message event handler."""        self.message_handlers.append({"event": event_type, "handler": handler})  # ============================================================# 5. RAG Frontend Pipeline# ============================================================class DocumentChunker:    """Split documents into chunks for RAG processing."""     def __init__(self, chunk_size=200, chunk_overlap=50):        self.chunk_size = chunk_size        self.chunk_overlap = chunk_overlap     def chunk_document(self, text, doc_id="doc_0"):        """Split document text into overlapping chunks."""        chunks = []        start = 0        chunk_idx = 0         while start < len(text):            end = start + self.chunk_size            chunk_text = text[start:end]             if chunk_text.strip():                chunks.append({                    "chunk_id": f"{doc_id}_chunk_{chunk_idx}",                    "doc_id": doc_id,                    "content": chunk_text.strip(),                    "start_char": start,                    "end_char": min(end, len(text)),                    "index": chunk_idx,                })                chunk_idx += 1             start = end - self.chunk_overlap            if start >= len(text):                break         return chunks  class SimpleVectorStore:    """Simple vector store using numpy for similarity search."""     def __init__(self, dim=64):        self.dim = dim        self.vectors = []        self.metadata = []        if HAS_NUMPY:            self.rng = np.random.RandomState(42)     def _text_to_vector(self, text):        """Convert text to a simple vector representation."""        if HAS_NUMPY:            vec = np.zeros(self.dim)            for i, char in enumerate(text.lower()):                idx = ord(char) % self.dim                vec[idx] += 1.0 / (1.0 + i * 0.01)            norm = np.linalg.norm(vec)            if norm > 0:                vec = vec / norm            return vec        else:            vec = [0.0] * self.dim            for i, char in enumerate(text.lower()):                idx = ord(char) % self.dim                vec[idx] += 1.0 / (1.0 + i * 0.01)            norm = sum(v * v for v in vec) ** 0.5            if norm > 0:                vec = [v / norm for v in vec]            return vec     def add_chunks(self, chunks):        """Add document chunks to the vector store."""        for chunk in chunks:            vec = self._text_to_vector(chunk["content"])            self.vectors.append(vec)            self.metadata.append(chunk)        return len(chunks)     def search(self, query, top_k=3):        """Search for most similar chunks to the query."""        query_vec = self._text_to_vector(query)         scores = []        for i, doc_vec in enumerate(self.vectors):            if HAS_NUMPY:                similarity = float(np.dot(query_vec, doc_vec))            else:                similarity = sum(a * b for a, b in zip(query_vec, doc_vec))            scores.append((similarity, i))         scores.sort(key=lambda x: x[0], reverse=True)        results = []        for score, idx in scores[:top_k]:            result = dict(self.metadata[idx])            result["similarity_score"] = round(score, 4)            results.append(result)         return results  class RAGPipeline:    """Complete RAG pipeline: upload -> chunk -> retrieve -> generate -> display."""     def __init__(self):        self.chunker = DocumentChunker(chunk_size=200, chunk_overlap=50)        self.vector_store = SimpleVectorStore(dim=64)        self.documents = {}        self.query_history = []     def upload_document(self, doc_id, content, metadata=None):        """Upload and process a document into the RAG system."""        self.documents[doc_id] = {            "content": content,            "metadata": metadata or {},            "char_count": len(content),        }         chunks = self.chunker.chunk_document(content, doc_id)        added = self.vector_store.add_chunks(chunks)         return {            "doc_id": doc_id,            "char_count": len(content),            "chunks_created": added,            "status": "indexed",        }     def query(self, question, top_k=3):        """Process a RAG query: retrieve relevant chunks and generate answer."""        retrieved = self.vector_store.search(question, top_k=top_k)         context_parts = [r["content"] for r in retrieved]        context_text = " ".join(context_parts)         answer = self._generate_answer(question, context_text, retrieved)         result = {            "question": question,            "answer": answer,            "sources": [                {"chunk_id": r["chunk_id"], "score": r["similarity_score"], "content": r["content"][:100]}                for r in retrieved            ],            "total_chunks_searched": len(self.vector_store.metadata),        }         self.query_history.append(result)        return result     def _generate_answer(self, question, context, retrieved_chunks):        """Generate answer from retrieved context (simulated)."""        if not retrieved_chunks:            return "I could not find relevant information to answer your question."         source_refs = ", ".join([r["chunk_id"] for r in retrieved_chunks[:3]])        key_phrases = context[:200]         return (            f"Based on the retrieved context, here is the answer to '{question}': "            f"The relevant information indicates: {key_phrases}... "            f"[Sources: {source_refs}]"        )     def get_stats(self):        """Get RAG pipeline statistics."""        return {            "total_documents": len(self.documents),            "total_chunks": len(self.vector_store.metadata),            "total_queries": len(self.query_history),            "documents": {                doc_id: {"chars": info["char_count"]}                for doc_id, info in self.documents.items()            },        }  # ============================================================# 6. Deployment Manager# ============================================================class KernelSession:    """Represents an isolated kernel session for a user."""     def __init__(self, session_id, user_id):        self.session_id = session_id        self.user_id = user_id        self.variables = {}        self.is_active = True        self.execution_count = 0        self.memory_usage_mb = 0.0     def execute(self, code):        """Simulate code execution in isolated kernel."""        self.execution_count += 1        self.memory_usage_mb += 0.5         if "=" in code and "==" not in code:            parts = code.split("=", 1)            var_name = parts[0].strip()            var_value = parts[1].strip()            self.variables[var_name] = var_value         return {            "session_id": self.session_id,            "execution_count": self.execution_count,            "status": "ok",            "memory_mb": self.memory_usage_mb,        }     def get_state(self):        """Return current kernel state."""        return {            "session_id": self.session_id,            "user_id": self.user_id,            "is_active": self.is_active,            "variables": list(self.variables.keys()),            "execution_count": self.execution_count,            "memory_mb": self.memory_usage_mb,        }  class DeploymentManager:    """Simulate Docker deployment and multi-user management."""     def __init__(self, app_name="mercury-app"):        self.app_name = app_name        self.sessions = {}        self.deploy_config = {}        self.user_quota = {"max_sessions_per_user": 3, "max_memory_mb": 512}        self.is_deployed = False     def configure_deployment(self, config):        """Configure Docker deployment settings."""        self.deploy_config = {            "app_name": config.get("app_name", self.app_name),            "notebook_path": config.get("notebook_path", "app.ipynb"),            "port": config.get("port", 5006),            "workers": config.get("workers", 4),            "docker_image": config.get("docker_image", "mljar/mercury:latest"),            "enable_auth": config.get("enable_auth", False),            "ssl": config.get("ssl", False),        }        return self.deploy_config     def deploy(self):        """Simulate Docker deployment."""        if not self.deploy_config:            self.configure_deployment({})         self.is_deployed = True        return {            "status": "deployed",            "app_name": self.deploy_config["app_name"],            "url": f"http://localhost:{self.deploy_config['port']}",            "docker_image": self.deploy_config["docker_image"],            "workers": self.deploy_config["workers"],        }     def create_session(self, user_id):        """Create a new isolated kernel session for a user."""        user_sessions = [s for s in self.sessions.values() if s.user_id == user_id]        if len(user_sessions) >= self.user_quota["max_sessions_per_user"]:            return {"error": "Maximum sessions reached for user", "user_id": user_id}         session_id = hashlib.md5(f"{user_id}:{len(self.sessions)}".encode()).hexdigest()[:12]        session = KernelSession(session_id, user_id)        self.sessions[session_id] = session         return {            "session_id": session_id,            "user_id": user_id,            "status": "created",            "active_sessions": len(self.sessions),        }     def execute_in_session(self, session_id, code):        """Execute code in a specific kernel session."""        if session_id not in self.sessions:            return {"error": f"Session '{session_id}' not found"}         session = self.sessions[session_id]        if not session.is_active:            return {"error": "Session is no longer active"}         return session.execute(code)     def terminate_session(self, session_id):        """Terminate a kernel session."""        if session_id in self.sessions:            self.sessions[session_id].is_active = False            return {"session_id": session_id, "status": "terminated"}        return {"error": "Session not found"}     def get_deployment_status(self):        """Get current deployment status."""        active = sum(1 for s in self.sessions.values() if s.is_active)        total_memory = sum(s.memory_usage_mb for s in self.sessions.values())         return {            "app_name": self.app_name,            "is_deployed": self.is_deployed,            "config": self.deploy_config,            "total_sessions": len(self.sessions),            "active_sessions": active,            "total_memory_mb": round(total_memory, 2),            "users": list(set(s.user_id for s in self.sessions.values())),        }  # ============================================================# Main Execution - Full Pipeline Demo# ============================================================def main():    print("=" * 60)    print("Mercury Notebook-to-Web Conversion & RAG Frontend Demo")    print("=" * 60)     # --- Step 1: YAML Config Parsing ---    print("\n[Step 1] Mercury YAML Config Parser")    print("-" * 40)     yaml_config = """query_text:    type: text    label: Search Query    value: What is RAGtop_k_slider:    type: slider    label: Number of Results    min: 1    max: 10    step: 1    value: 3model_select:    type: select    label: LLM Model    choices: [gpt-4, gpt-3.5, llama2, mistral]    value: gpt-4file_input:    type: file_upload    label: Upload Document    accept: .pdf,.txt,.md    max_size: 5242880enable_cache:    type: checkbox    label: Enable Response Cache    value: true"""     parser = MercuryYAMLParser()    widgets = parser.parse_yaml_config(yaml_config)    print(f"Parsed {len(widgets)} widget definitions:")    for name, wdef in widgets.items():        print(f"  - {name}: type={wdef['type']}, label={wdef.get('label', 'N/A')}")     # --- Step 2: Notebook-to-Web Conversion ---    print("\n[Step 2] Notebook-to-Web Converter")    print("-" * 40)     notebook = {        "cells": [            {                "cell_type": "markdown",                "source": ["# Mercury RAG Application\n", "Interactive document Q&A system"],                "metadata": {},            },            {                "cell_type": "code",                "source": ["import numpy as np\n", "print('Modules loaded')"],                "metadata": {"hide_code": True, "cell_id": "cell_import"},            },            {                "cell_type": "code",                "source": ["query = 'What is retrieval augmented generation'\n", "top_k = 3"],                "metadata": {"hide_code": False, "cell_id": "cell_params"},            },            {                "cell_type": "code",                "source": ["results = search_documents(query, top_k)\n", "print('Search complete')"],                "metadata": {"hide_code": False, "cell_id": "cell_search"},            },        ]    }     converter = NotebookToWebConverter()    cell_count = converter.load_notebook(notebook)    converter.parse_widget_config(yaml_config)    app_structure = converter.convert()     print(f"Converted {cell_count} cells to web components")    print(f"Interactive cells: {app_structure['interactive_cells']}")    print(f"Total widgets: {len(app_structure['widgets'])}")     # --- Step 3: Widget System ---    print("\n[Step 3] Widget System - Interactive State Management")    print("-" * 40)     widget_mgr = WidgetManager()    for name, config in widgets.items():        widget_mgr.register_widget(name, config)     result = widget_mgr.simulate_interaction("top_k_slider", 5)    print(f"Slider interaction: {result}")     result = widget_mgr.simulate_interaction("model_select", "llama2")    print(f"Select interaction: {result}")     result = widget_mgr.simulate_interaction("query_text", "Explain vector embeddings")    print(f"Text interaction: {result}")     all_values = widget_mgr.get_all_values()    print(f"Current widget state: {json.dumps(all_values, indent=2, ensure_ascii=False)}")     # --- Step 4: Chat Component ---    print("\n[Step 4] Mercury Chat Component")    print("-" * 40)     chat = MercuryChatComponent(system_prompt="You are a RAG assistant for document Q&A.")    sys_msg = chat.add_system_message()    print(f"System message initialized: {sys_msg['role']}")     response1 = chat.send_message("Hello, what can you help me with?")    print(f"User: {response1['user_message']['content']}")    print(f"Assistant: {response1['assistant_message']['content']}")     response2 = chat.send_message(        "What is retrieval augmented generation?",        context={            "documents": [                {"content": "RAG combines retrieval with generation to produce grounded answers from documents"},                {"content": "Vector embeddings enable semantic search over document chunks"},            ],            "sources": ["doc1.pdf", "doc2.pdf"],        },    )    print(f"User: {response2['user_message']['content']}")    print(f"Assistant: {response2['assistant_message']['content'][:100]}...")    print(f"Total messages in history: {response2['total_messages']}")     # --- Step 5: RAG Pipeline ---    print("\n[Step 5] RAG Frontend Pipeline")    print("-" * 40)     rag = RAGPipeline()     doc1 = (        "Retrieval Augmented Generation (RAG) is a technique that combines information retrieval "        "with large language model generation. It first retrieves relevant document chunks from a "        "knowledge base, then uses these chunks as context for the language model to generate "        "accurate and grounded responses. RAG reduces hallucination by grounding generation in "        "retrieved facts."    )     doc2 = (        "Mercury is a Python framework that allows building web applications directly from "        "Jupyter Notebooks. It uses YAML configuration to define interactive widgets like sliders, "        "text inputs, and file uploads. When a user interacts with a widget, Mercury re-executes "        "the notebook with updated parameters, making it ideal for rapid prototyping of data "        "applications and RAG interfaces."    )     doc3 = (        "Docker deployment of Mercury applications provides kernel isolation between users. "        "Each user session runs in a separate kernel, ensuring that variable state and execution "        "do not interfere across users. The deployment manager handles session lifecycle, memory "        "limits, and concurrent access management for production deployments."    )     upload1 = rag.upload_document("rag_intro", doc1, {"source": "textbook"})    upload2 = rag.upload_document("mercury_guide", doc2, {"source": "tutorial"})    upload3 = rag.upload_document("deployment_guide", doc3, {"source": "manual"})    print(f"Document 1: {upload1['chunks_created']} chunks created")    print(f"Document 2: {upload2['chunks_created']} chunks created")    print(f"Document 3: {upload3['chunks_created']} chunks created")     query_result = rag.query("How does Mercury build web applications from notebooks?")    print(f"\nQuery: {query_result['question']}")    print(f"Answer: {query_result['answer'][:150]}...")    print(f"Sources found: {len(query_result['sources'])}")    for src in query_result["sources"]:        print(f"  - {src['chunk_id']} (score: {src['score']})")     stats = rag.get_stats()    print(f"\nRAG Stats: {stats['total_documents']} docs, {stats['total_chunks']} chunks, {stats['total_queries']} queries")     # --- Step 6: Deployment Manager ---    print("\n[Step 6] Deployment Manager - Docker & Multi-User")    print("-" * 40)     deployer = DeploymentManager(app_name="rag-mercury-app")    config = deployer.configure_deployment({        "app_name": "rag-mercury-app",        "notebook_path": "rag_app.ipynb",        "port": 8080,        "workers": 4,        "docker_image": "mljar/mercury:latest",        "enable_auth": True,        "ssl": True,    })    print(f"Deployment configured: {config['app_name']} on port {config['port']}")     deploy_result = deployer.deploy()    print(f"Deployed at: {deploy_result['url']}")    print(f"Docker image: {deploy_result['docker_image']}")     session1 = deployer.create_session("user_alice")    session2 = deployer.create_session("user_bob")    session3 = deployer.create_session("user_alice")    print(f"\nSession 1 (Alice): {session1['session_id']}")    print(f"Session 2 (Bob): {session2['session_id']}")    print(f"Session 3 (Alice): {session3['session_id']}")     exec_result = deployer.execute_in_session(session1["session_id"], "query = 'test query'")    print(f"\nExecution in Alice's session: count={exec_result['execution_count']}, memory={exec_result['memory_mb']}MB")     status = deployer.get_deployment_status()    print(f"\nDeployment Status:")    print(f"  App: {status['app_name']}")    print(f"  Deployed: {status['is_deployed']}")    print(f"  Active sessions: {status['active_sessions']}")    print(f"  Total memory: {status['total_memory_mb']}MB")    print(f"  Users: {status['users']}")     print("\n" + "=" * 60)    print("Demo complete. All Mercury components simulated successfully.")    print("=" * 60)  if __name__ == "__main__":    main()
目的:模拟Mercury框架的核心功能模块,包括YAML配置解析、Notebook转Web转换、Widget交互管理、聊天组件消息流处理、RAG检索增强生成管道和Docker部署管理,展示了从Notebook到可交互Web应用的完整技术链路
  1. class MercuryYAMLParser:定义Mercury YAML配置解析器类,支持text/slider/select等多种Widget类型
  2. class NotebookToWebConverter:定义Notebook到Web应用的转换器类,负责将Cell转换为Web组件
  3. class WidgetManager:定义Widget管理器类,管理交互组件的状态变更和验证
  4. class MercuryChatComponent:定义Mercury聊天组件类,处理消息流和RAG上下文集成
  5. class RAGPipeline:定义完整的RAG管道类,包含上传分块检索生成全流程
  6. class DeploymentManager:定义部署管理器类,管理Docker部署和多用户Kernel隔离
  7. def parse_yaml_config(self, yaml_string):解析YAML配置字符串,提取Widget定义并验证参数类型
  8. def convert(self):执行Notebook到Web的转换,生成包含所有组件的应用结构
  9. def send_message(self, user_input, context=None):处理用户消息并生成带RAG上下文的助手回复
  10. def query(self, question, top_k=3):执行RAG查询:检索相关文档片段并生成带引用的回答

常见问答

Mercury与Voila在Notebook转Web方面的核心架构区别是什么?
Mercury采用YAML声明式配置定义交互组件,Widget参数在Notebook执行前注入为Python变量;Voila依赖ipywidgets在代码中创建交互组件,使用Jupyter Widgets协议进行前后端通信。Mercury每次交互重新执行整个Notebook,而Voila支持基于Widget状态的增量更新。Mercury的配置方式更适合数据科学家,Voila的代码方式更适合有前端经验的开发者。
Mercury每次交互都重新执行整个Notebook,这对RAG应用有什么影响?如何优化?
影响:每次用户修改Widget值或发送消息时,整个Notebook从头执行,包括文档加载、分块、向量化等耗时操作,导致响应延迟。优化策略:1)将文档索引缓存到磁盘,仅在内容变化时重建;2)将模型加载放在Notebook开头并使用全局缓存;3)使用持久化存储(如向量数据库)避免每次重建索引;4)将耗时操作和交互操作分离到不同Notebook;5)合理设置chunk_size平衡检索质量和执行速度。
Mercury的Kernel隔离机制在多用户RAG应用中如何工作?有哪些资源挑战?
每个用户会话分配独立的Jupyter Kernel(Python进程),变量空间和内存完全隔离,防止用户间干扰。资源挑战:1)每个Kernel都是独立进程,内存占用高;2)如果每个Kernel都加载向量索引和LLM模型,内存消耗成倍增长;3)Kernel数量受服务器资源限制。解决方案:使用共享内存映射存储只读索引、使用模型服务替代内嵌模型、设置Kernel内存限制和超时回收。
在Mercury中实现RAG聊天组件时,如何处理对话历史和上下文连续性?
由于Mercury每次交互重新执行Notebook,对话历史无法通过内存变量保持。解决方案:1)使用文件系统存储对话历史(JSON或SQLite),每次执行时加载;2)将对话历史作为隐藏Widget参数传递;3)使用外部数据库(如Redis)存储会话状态;4)在Notebook开头添加状态恢复逻辑,从持久化存储加载历史消息。需要注意并发写入的同步问题和存储容量的管理。
Mercury的Widget系统支持哪些类型?如何在RAG应用中合理配置这些Widget?
Mercury支持7种Widget类型:text(文本输入)、textarea(多行文本)、slider(滑块)、select(下拉选择)、checkbox(复选框)、file_upload(文件上传)和numeric(数值输入)。RAG应用典型配置:text用于用户查询输入;slider用于控制top_k(检索数量)和chunk_size(分块大小);select用于切换LLM模型和检索策略;file_upload用于文档导入;checkbox用于开关缓存和流式输出;numeric用于设置温度和最大token数。
Mercury适合什么样的RAG应用场景?不适合什么场景?
适合:1)RAG原型快速验证和迭代;2)内部团队小规模的文档问答测试;3)RAG教学和演示;4)A/B测试不同的RAG策略。不适合:1)面向大量外部用户的生产系统(并发能力有限);2)大规模文档集合(数万文档以上)的实时检索(性能瓶颈);3)需要复杂前端交互(如拖拽、实时图表)的场景;4)需要毫秒级响应延迟的实时应用。原型验证后应迁移到LangServe或FastAPI等生产级框架。

常见误区

误区认为Mercury的交互组件和Voila一样使用ipywidgets代码实现

正解Mercury的交互组件通过YAML配置而非ipywidgets其参数在Notebook执行前注入为变量开发者需在YAML中定义参数类型和默认值不能直接用ipywidgets代码

为何常见:Voila和Mercury都是Notebook转Web工具容易混淆两者的交互组件实现方式

误区认为Mercury应用的交互响应是即时的因为它只是更新前端显示

正解Mercury每次用户交互会重新执行整个Notebook而非增量更新对于长Notebook会导致较长等待时间需优化Notebook结构和预计算不能假设交互是即时的

为何常见:Web应用的交互直觉是即时响应容易忽略Mercury每次交互触发完整Notebook重新执行的机制

误区认为Mercury自带RAG能力只需配置即可实现文档问答功能

正解Mercury的聊天组件需要配合后端LLM API实现RAG它本身不提供AI能力只是前端交互框架需在Notebook中调用检索和生成API不能把Mercury当作完整的RAG解决方案

为何常见:Mercury的聊天组件让人误以为它内置AI能力忽略了它只是前端框架需要配合后端API使用

参考来源