核心挑战:如何为复杂数据分析任务构建可扩展的代码沙箱工具?本文将以E2B沙箱为例,通过对比Low-Level与FastMCP两种MCP-Server实现方案,深入剖析:

  • Resource/Tool/Prompt的高阶应用场景
  • 数据分析coding任务的难点和解决方案
  • FastMCP在原有mcp-server的基础上做了哪些开发简化

Cluade的经典Demo多是用low level构建,而在最新版本中推出了高级FastMCP,但是工程上的简化,意味着很多功能被隐藏,所以更适合从low level一步步走过来对MCP设计每一步都很清晰得高级使用用户,而非新手小白。

这里我选了基于E2B的coding沙箱来搭建MCP server,刚好为下一章我们手搓数据分析智能体做个铺垫,完整代码详见: DAAgent

Coding MCP

真实场景的复杂性:生产级数据分析智能体中的Coding工具远非Python REPL可胜任,核心痛点在于三类数据流的信息传递:

数据流类型 挑战场景 本方案解决思路
code2code 多段代码块变量传递 持久化存储(CSV/JSON)
code2llm 执行上下文(stdout/error) 结构化日志捕获
code2human 结果可视化 Jupyter Notebook组装

那基于以上3点考量,我们要设计的coding MCP server就需要具备以下功能

  • sandbox initialize & stop:沙箱创建和销毁
  • upload file:数据分析场景往往依赖前置输出的csv数据文件
  • execude code:最后才是coding工具默认拥有的代码执行工具

和其他的一些sandbox mcp相比在工具设计上有几点不同

常见功能 本方案实现方式 选择依据
库安装 利用kernle内核内嵌!pip Coding一致性更好,很多library是在code运行过程中发现的,而非在coding之前制定
文件下载 捕捉所有多模态的执行结果直接返回 对输出文件的目录管理会更加灵活
单步代码执行 显式create/destroy沙箱支持多步coding 复杂任务通过多步coding传递中间变量和持久化文件,同时避免多次pip的时间浪费

Low-Level Server

下面我们先用Low-level Server框架来设计Coding MCP。按前面的设计,我们分别需要initialize sandbox,stop sandbox,upload file,execute code这四个工具。

除了执行代码之外的前三个工具,它们都可以使用文本输出格式,他们传递给模型的信息相对简单就是是否成功创建、销毁沙箱、以及是否成功上传文件,以及对应的沙箱ID标识和文件上传目录。所以这三个工具我选择了文本格式的输出。代码执行工具这里,我选择了结构化输出,这样可以更有效的拆分代码执行后的各种不同日志类型,方面后面的coding步骤定位修复问题。

需要注意的是,构建MCP 工具时不仅要全面完整输出成功日志,对于报错也要有详细的日志输出,必要时需要返回traceback,否则模型无法获取完整上下文很难修正工具调用方式。

以下是code 功能层核心代码实现


# 工具函数定义
async def initialize_sandbox(timeout: int = Field(description='沙箱最大运行时长,单位为秒', default=1000)) -> str:
"""创建一个新的沙箱环境用于代码执行
"""
global Active_Sandboxes
session_id = str(uuid.uuid4())
logger.info(f'创建沙箱中:session_id = {session_id}')
try:
sandbox = Sandbox(api_key=os.getenv("E2B_API_KEY"), timeout=timeout)
Active_Sandboxes[session_id] = sandbox
except Exception as e:
msg = f'Failed to initialize sandbox: {str(e)}'
logger.warning(e)
return msg
msg = f"Sandbox initialized successfully with session ID: {session_id}"
logger.info(msg)
return msg async def close_sandbox(session_id: str = Field(description='需要关闭的沙箱id')) -> str:
"""所有代码运行完毕之后,关闭已有的沙箱环境
:param session_id: 沙箱唯一标识符
:return: 执行日志
"""
global Active_Sandboxes
if session_id in Active_Sandboxes:
sandbox = Active_Sandboxes[session_id]
try:
sandbox.kill()
except Exception as e:
msg = f'Failed to close sandbox with session ID: {session_id}, {str(e)}'
logger.warning(e)
return msg
msg = f"Sandbox close successfully with session ID: {session_id}"
logger.info(msg)
return msg
else:
msg = f"Sandbox failed to stop session ID: {session_id} not found."
logger.warning(msg)
return msg class CodeOutput(BaseModel):
stdout: str
stderr: str
error: str
traceback: str async def execute_code(session_id: str = Field(description='需要运行代码的目标沙箱id'),
code: str = Field(description='待运行的代码')) -> CodeOutput:
"""在沙箱中运行代码并获取代码的所有返回结果,包括stdout、stderr、各类持久化输出、图片等等
:param session_id: 沙箱唯一标识符
:param code: 待执行的代码
:return: 代码执行返回结构的结构体
"""
global Active_Sandboxes, Execution_DB
if session_id not in Active_Sandboxes:
msg = f'Sandbox with session ID : {session_id} not found'
data = {'error': msg}
else:
try:
sandbox = Active_Sandboxes[session_id]
# sandbox会在server内提供流式的内容输出
execution = sandbox.run_code(
code,
on_stdout=lambda data: logger.info(data),
on_stderr=lambda data: logger.info(data),
on_error=lambda data: logger.info(data)
) data = CodeOutput(
stdout=''.join(execution.logs.stdout),
stderr=''.join(execution.logs.stderr),
error=str(execution.error) if execution.error else '',
traceback=execution.error.traceback if execution.error else '',
)
# Store execution record in database
execution_record = {
'timestamp': datetime.now().isoformat(),
'code': code,
'output': execution # 存储沙箱原始结果即可
}
Execution_DB[session_id]['executions'].append(execution_record)
logger.info(f"Stored execution record for session {session_id}")
except Exception as e:
error_msg = f"Code execution failed: {str(e)}"
traceback_str = traceback.format_exc()
logger.warning(f"Execution error: {error_msg}\n{traceback_str}")
data = CodeOutput(
error=error_msg,
traceback=traceback_str,
stdout='',
stderr=''
)
execution_record = {
'timestamp': datetime.now().isoformat(),
'code': code,
'output': None
}
Execution_DB[session_id]['executions'].append(execution_record)
logger.info(f"Stored error execution record for session {session_id}") return data async def upload_file(session_id: str = Field(description='待上传文件的目标沙箱id'),
file_list: List = Field(description='上传文件列表,每个都是本地文件的绝对路径')
) -> str:
"""上传本地文件到当前正在执行的沙箱中
:param session_id: 沙箱唯一标识ID
:param file_list: 待上传的本地文件列表,文件名称包含本地文件路径
:return: 执行日志
"""
global Active_Sandboxes
if session_id not in Active_Sandboxes:
msg = f'Sandbox with session ID : {session_id} not found'
logger.warning(msg)
return msg
else:
sandbox = Active_Sandboxes[session_id]
return_msg = ''
for file in file_list:
try:
with open(file, 'rb') as f:
sandbox.files.write(file, f.read())
msg = f'Uploading {file} success'
logger.info(msg)
except Exception as e:
msg = f'Uploading {file} failed:' + str(e)
logger.warning(msg)
return_msg += msg + '\n'
return return_msg

下面是low level Server核心list_tool和call_tool的实现,需要用户显式定义每一个工具的schema和具体的调用方法, 以下我已经参考后面的FastMCP的高级封装在定义时做了一些简化。每个工具的名称和描述,直接来自函数名称和注释,不用再手工填写。

@server.list_tools()
async def list_tools() -> list[Tool]:
tools = [
Tool(name=initialize_sandbox.__name__,
description=initialize_sandbox.__doc__,
inputSchema={
"type": "object",
"properties": {
"timeout": {
"type": "integer",
"description": "沙箱最大运行时长,单位为秒"
}
},
"required": ["timeout"],
}),
Tool(name=close_sandbox.__name__,
description=close_sandbox.__doc__,
inputSchema={
"type": "object",
"properties": {
"session_id": {
"type": "string",
"description": "需要关闭的沙箱id"
}
},
"required": ["session_id"],
}),
Tool(name=execute_code.__name__,
description=execute_code.__doc__,
inputSchema={
"type": "object",
"properties": {
"session_id": {
"type": "string",
"description": "待运行代码的沙箱id"
},
"code": {
"type": "string",
"description": '待运行的代码'
}
},
"required": ["session_id", "code"],
},
outputSchema=CodeOutput.model_json_schema()),
Tool(name=upload_file.__name__,
description=upload_file.__doc__,
inputSchema={
"type": "object",
"properties": {
"session_id": {
"type": "string",
"description": "待上传文件的目标沙箱id"
},
"file_list": {
"type": "array",
"description": "上传文件列表,每个都是本地文件的绝对路径"
}
},
"required": ["session_id", "file_list"]
}) ] return tools @server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]):
"""
call tool返回结构并不局限在Text, Audio 和 Image类型, 如果你定义复杂结构体,返回Any类型即可
"""
try:
match name:
case initialize_sandbox.__name__:
result = await initialize_sandbox(arguments['timeout'])
result = [TextContent(type="text", text=result)] case close_sandbox.__name__:
result = await close_sandbox(arguments['session_id'])
result = [TextContent(type="text", text=result)] case upload_file.__name__:
result = await upload_file(arguments['session_id'], arguments['file_list'])
result = [TextContent(type="text", text=result)] case execute_code.__name__:
result = await execute_code(arguments['session_id'], arguments['code'])
result = result.dict()
case _:
raise ValueError('Error calling tool: input name do not match any tool name')
return result
except Exception as e:
raise ValueError(f'Error calling tool: {str(e)}') async def main():
options = server.create_initialization_options()
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, options) if __name__ == '__main__':
import asyncio
asyncio.run(main())

唯一在开发时让我有些困惑的在于call_tool的返回类型,因为需要兼容结构化输出和纯文本输出这两种类型,细看server的Type Hint会发现call_tool的输出类型是Sequence[ContentBlock] | dict[str, Any], 也就是结构化和非结构化(纯文本)的输出类型分别是List和Dict两种类型,会被解析到两个不同的字段contentstructuredContent

本质上是否以文本格式返回的争议点在于工具返回内容的使用方是工具?模型?人类?,纯文本默认了工具的使用方是模型,但其实很多场景下并不是,例如多步工具调用传递参数、工具作为程序控制器、编程场景、数据分析场景等等。于是官方代码后面才把StructuredContent加上,git上有不少讨论,感兴趣的可以去瞅瞅Bring back the concept of "toolResult" (non-chat result)

High-Level FastMCP

有了Low-Level API的基础FastMCP就好理解多了。以下是效果相同,基于FastMCP这个High Level API的MCP工具实现,code功能部分和以上相同就不重复了。


from mcp.server.fastmcp import FastMCP logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("e2b-sandbox") # 服务初始化
mcp = FastMCP("code-sandbox")
Active_Sandboxes = {}
load_dotenv() # 读取env文件中的E2B API Key @mcp.tool()
async def initialize_sandbox(timeout: int = Field(description='沙箱最大运行市场,单位为秒', default=1000)) -> str:
"""创建一个新的沙箱环境用于代码执行
:return: str: 返回初始化日志包含启动沙箱id
"""
...
return msg @mcp.tool()
async def close_sandbox(session_id: str = Field(description='需要关闭的沙箱id')) -> str:
"""所有代码运行完毕之后,关闭已有的沙箱环境
:return: 执行日志
"""
...
return msg class CodeOutput(TypedDict):
results: List
stdout: str
stderr: str
error: str
traceback: str @mcp.tool()
async def execute_code(session_id: str = Field(description='需要运行代码的沙箱id'),
code: str = Field(description='待运行的代码')) -> CodeOutput:
"""在沙箱中运行代码并获取代码的所有返回结果
:return: 代码执行返回结构体包含stdout,stderr,error
"""
...
return data @mcp.tool()
async def upload_file(session_id: str = Field(description='上传到目标沙箱的唯一标识符'),
file_list: List = Field(description='上传文件列表,每个都是本地文件的绝对路径')
) -> str:
"""上传本地文件到当前正在执行的沙箱中
:return: 执行日志
"""
...
return return_msg if __name__ == '__main__':
mcp.run(transport='stdio')

从中我们可以发现FastMCP在服务开发上做了以下几点简化

  1. 服务器创建和启动的简化:FastMCP可以像flask、FastAPI一样直接mcp.run启动
  2. 工具(资源、prompt)定义和调用的简化:无需显式构建list_tool和call_tool API,使用mcp.tool装饰器直接注册工具,背后的tool Manager会自动完成以上工具说明和工具调用方法的构建,resource和prompt也是同理。
  3. 上下文和日志的简化:看实现FastMCP抽象了Context对象来简化日志和进度管理,但这块我还没具体用过就不做展开。

⚙️ 简单说下FastMCP的架构核心:

  1. FastMCP引入了3个核心管理器Tool、Resource、Prompt Manger:每个manager内部都各自实现了注册(add_tool)、获取(list_tool)、调用(call_tool)的功能,共同维护服务内所有的tool、resource和prompt
class FastMCP:
def __init__(self, ...):
self._tool_manager = ToolManager(...)
self._resource_manager = ResourceManager(...)
self._prompt_manager = PromptManager(...)
  1. FastMCP通过装饰器和自动类型推断简化注册:在装饰器注册工具后,通过typing和pydantic BaseModeln能自动完成工具输入参数和输出类型的解析直接生成list_tool的schema。并且对比low-level,FastMCP基于pydantic提供了更多参数验证的功能。
def tool(self, name=None, title=None, description=None):
def decorator(fn):
# 自动分析函数签名
func_metadata = func_metadata(fn) # 自动生成参数schema
parameters = func_metadata.arg_model.model_json_schema() # 注册到工具管理器
self._tool_manager.add_tool(fn, name, title, description)
return fn
return decorator

考虑到FastMCP开发的便捷性,后面再进一步的MCP开发我们都会基于FastMCP开发,不再演示low-level server的实现(哈哈哈用low-level的耐心已耗尽)。

MCP用法脑暴

Prompt有啥用?

和工具不同,Prompt是用户手动选择,而非模型选择的。 本质上只是Server提供者给到的一些工具指令的最佳实践而已,mcp的使用者可以选择用或者不用。

不过一个有意思的使用方式,是在团队内部可以整一个prompt-mcp-server来把大家在平时开发中亲测好用的一些prompt累积起来,避免各自为战和重复劳动,哈哈准备搞起来~

Resource vs Tool

Tool和Prompt的用法相对常见,但是Resource的存在一度让我很难理解,我看到了包括以下的几种解释

  • Get vs Post:Tool和Resource的区别就是Get和Post的区别,工具可以执行任务而Resource只能获取信息。谁说Tool不能用Get方法了?
  • 和文件或数据库对接返回数据:Resource专用于和文件和数据库对接,用于返回模型需要的数据。Tool不是一样也能对接数据库和系统文件?

以上的说法并没说服我,看了看多server也只发现Resource和tool之前还有以下几点不同

  • 订阅和推送:Resource可以实现订阅推送机制,从模型主动获取数据变更,到检测数据变更并推送给模型,但是这依赖客户端是否有类似的设计,需要客户端订阅resource变更
  • 多模态数据类型:还是前面的思路tool的返回结果默认是传递给模型的,因此以文本作为主要格式,后面才加入了structureOutput,而resource则支持数据流、二进制等更多数据类型

个人感觉resource的使用特性还在摸索阶段,我还没太想明白哈哈哈,要是大家有好的思路以欢迎评论区留言~

在server里面我设计了一个jupyter resource,用于当整个任务完成后,把所有code和code执行结果打包成jupyter notebook回传给客户端,进行展示。这里我选择把jupyter notebook按照nb4的格式返回json字符串的思路,当然返回二进制流也是可以的。

server部分的代码如下,在生产场景一般这里会使用OSS等远程存储,直接把ipynb上传到云,在客户端再直接使用返回的链接去云上下载文件即可,这里咱都在本地就简化成回传文件信息了~

@mcp.resource("file://notebook/{session_id}.ipynb",
mime_type='application/json')
async def get_execution_history(session_id: str) -> str:
"""获取指定session的所有代码和代码执行历史记录,以Jupyter Meta Data形式返回
"""
global Execution_DB
cell_list = []
code_counter = 0
logger.info('get jupyter history')
if session_id in Execution_DB:
session_data = Execution_DB[session_id]
executions = session_data.get('executions', [])
for execution_record in executions:
cell_list.append({
"cell_type": "code",
"execution_count": code_counter,
"metadata": {},
"source": execution_record['code'],
"outputs": parse_nb(execution_record["output"])
})
code_counter += 1
notebook = update_notebook_data(cell_list) logger.info(f"Generated notebook for session {session_id}")
# 返回notebook的JSON内容作为bytes,这样客户端会接收到真正的ipynb文件内容
return json.dumps(notebook,ensure_ascii=False)
else:
raise ValueError(f'Sandbox with session ID : {session_id} not found')

MCP Client

说完服务端,咱最后来看下客户端,需要注意的是low-level mcp和high-level FastMCP接口也存在细微差异,简单说fastmcp返回值嵌套减少了一层,考虑后面都会以FastMCP为主,这里只展示FastMCP对应client的相关代码和结果

from fastmcp import Client
from fastmcp.client.transports import StdioTransport # 使用Transport的原因为为了制定server.py脚本的路径
transport = StdioTransport(
command="python",
args=["-m", "src.servers.e2b_high_level.server"],
env={"LOG_LEVEL": "DEBUG"}
)
session = Client(transport) async with session:
# List available prompts
prompts = await session.list_prompts()
print(f"Available prompts: {[p.name for p in prompts]}") # List available resources:静态resource
resources = await session.list_resources()
print(f"Available resources: {[r.uri for r in resources]}") # List available resource templates:动态resource
templates = await session.list_resource_templates()
print(f"Available templates: {[r.uriTemplate for r in templates]}") # List available tools
tools = await session.list_tools()
print(tools)
print(f"Available tools: {[t.name for t in tools]}") # 初始化沙箱
result = await session.call_tool("initialize_sandbox", arguments={"timeout": 1000})
print(f"Tool result: {result.content[0].text}")
# 实际上session_id应该由模型基于上文,在下一步execute code的工具调用中推理得到
session_id = result.content[0].text.split(':')[1].strip()
# 上传文件 current_dir = os.path.dirname(os.path.abspath(__file__))
result = await session.call_tool("upload_file",
arguments={"session_id": session_id,
"file_list": [os.path.join(current_dir, 'tests',
'fund_information.csv')]})
print(f"Tool result: {result.content[0].text}") # 执行2步代码: 对于结构化输出,call_tool内部会根据tool的output schema,自动进行model validate解析
result = await session.call_tool("execute_code",
arguments={"session_id": session_id, "code": test_code1})
print(f"Execute Code: {result.structured_content}") result = await session.call_tool("execute_code",
arguments={"session_id": session_id, "code": test_code2})
print(f"Execute Code: {result.structured_content}") # 直接获取notebook文件内容 - 使用application/octet-stream
notebook_resource = await session.read_resource(f'file://notebook/{session_id}.ipynb')
with open( f'notebook_{session_id}.ipynb', 'w',encoding='UTF-8') as f:
f.write(notebook_resource[0].text) # 关闭沙箱
result = await session.call_tool("close_sandbox", arguments={"session_id": session_id})
print(f"Tool result: {result.content[0].text}")

结果如下

使用资源获取的notebook文件如下


学习资源列表

想看更全的大模型论文·Agent·开源框架·AIGC应用 >> DecryPrompt

解密prompt系列59. MCP实战:从Low-Level到FastMCP的搭建演进的更多相关文章

  1. 解密Prompt系列6. lora指令微调扣细节-请冷静,1个小时真不够~

    上一章介绍了如何基于APE+SELF自动化构建指令微调样本.这一章咱就把微调跑起来,主要介绍以Lora为首的低参数微调原理,环境配置,微调代码,以及大模型训练中显存和耗时优化的相关技术细节 标题这样写 ...

  2. 解密prompt系列5. APE+SELF=自动化指令集构建代码实现

    上一章我们介绍了不同的指令微调方案, 这一章我们介绍如何降低指令数据集的人工标注成本!这样每个人都可以构建自己的专属指令集, 哈哈当然我也在造数据集进行时~ 介绍两种方案SELF Instruct和A ...

  3. 解密Prompt系列2. 冻结Prompt微调LM: T5 & PET & LM-BFF

    这一章我们介绍固定prompt微调LM的相关模型,他们的特点都是针对不同的下游任务设计不同的prompt模板,在微调过程中固定模板对预训练模型进行微调.以下按时间顺序介绍,支持任意NLP任务的T5,针 ...

  4. 解密Prompt系列3. 冻结LM微调Prompt: Prefix-Tuning & Prompt-Tuning & P-Tuning

    这一章我们介绍在下游任务微调中固定LM参数,只微调Prompt的相关模型.这类模型的优势很直观就是微调的参数量小,能大幅降低LLM的微调参数量,是轻量级的微调替代品.和前两章微调LM和全部冻结的pro ...

  5. 解密Prompt系列4. 升级Instruction Tuning:Flan/T0/InstructGPT/TKInstruct

    这一章我们聊聊指令微调,指令微调和前3章介绍的prompt有什么关系呢?哈哈只要你细品,你就会发现大家对prompt和instruction的定义存在些出入,部分认为instruction是promp ...

  6. 解密SVM系列(二):SVM的理论基础(转载)

    解密SVM系列(二):SVM的理论基础     原文博主讲解地太好了  收藏下 解密SVM系列(三):SMO算法原理与实战求解 支持向量机通俗导论(理解SVM的三层境界) 上节我们探讨了关于拉格朗日乘 ...

  7. Java 加解密技术系列文章

    Java 加解密技术系列之 总结 Java 加解密技术系列之 DH Java 加解密技术系列之 RSA Java 加解密技术系列之 PBE Java 加解密技术系列之 AES Java 加解密技术系列 ...

  8. 11.Java 加解密技术系列之 总结

    Java 加解密技术系列之 总结 序 背景 分类 常用算法 原理 关于代码 结束语 序 上一篇文章中简单的介绍了第二种非对称加密算法 — — DH,这种算法也经常被叫做密钥交换协议,它主要是针对密钥的 ...

  9. 10.Java 加解密技术系列之 DH

    Java 加解密技术系列之 DH 序 概念 原理 代码实现 结果 结束语 序 上一篇文章中简单的介绍了一种非对称加密算法 — — RSA,今天这篇文章,继续介绍另一种非对称加密算法 — — DH.当然 ...

  10. 9.Java 加解密技术系列之 RSA

    Java 加解密技术系列之 RSA 序 概念 工作流程 RSA 代码实现 加解密结果 结束语 序 距 离上一次写博客感觉已经很长时间了,先吐槽一下,这个月以来,公司一直在加班,又是发版.上线,又是新项 ...

随机推荐

  1. 【Spring Boot】ActiveMQ 连接池

            spring.activemq.pool.enabled=false时,每发送一条数据都需要创建一个连接,这样会出现频繁创建和销毁连接的场景.为了不踩这个坑,我们参考池化技术的思想,配 ...

  2. CommonJS、ES 导出和导入模块

    以下代码制作展示,不能直接运行. CommonJS导出 // module.cjs // CJS默认导出 //module.exports = 'Hello world'; /*module.expo ...

  3. Es简单条件查询

    一:先看一下es的语句以及查询结果:  我这边使用的条件是is_device要么是工控要么是资产 二:java代码部分 关于es的操作,java里面不需要添加mapper层,只要在service以及c ...

  4. 探索学习Hypermesh的有效方法

    大家好!我是一名Hypermesh的学习者,最近在学习这个强大的有限元前处理软件时,总结了一些有效的学习方法,希望能与大家分享. 1. 熟悉软件界面和工具:首先,我们需要熟悉Hypermesh的界面和 ...

  5. idea maven 打包错误 [ERROR] javac options source files

    [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-c ...

  6. 实现对C语言类学生管理系统文件存储的两种方法

    学习javascript的时候曾经想做一个留言板的应用,但是却由于不知道如何存储失败了,由于做这个留言板的思路类似于C语言的学生管理系统,故此这次经历让我重新审视自己去学懂C语言的文件操作. 我重新用 ...

  7. typescript结构化类型应用两例

    介绍 结构化类型是typescript类型系统的一个重要特性,如果不了解这个特性,则经常会被typescript的行为搞得一头雾水,导致我们期待的行为与实际的行为不一致.今天我们就来看两个例子. 不了 ...

  8. Milvus 使用

    Milvus记录:1.安装Python: $ pip install -U pymilvus #pymilvus 中包含的一个 python 库,可以嵌入到客户端应用程序中 $ pip install ...

  9. 如何打开超过20G的sql文件

    最近在导项目中数据的流程时,对原数据库做了备份,备份好的sql文件有40G左右,通过一番折腾把数据库拿到本地PC,发现PC机直接弄不行: 平时经常使用的NotePad++,提示文本太大无法打开: PC ...

  10. 浅谈AXI协议及搭建自己的AXI IP核-01(协议解读)

    一.什么是AXI协议? AXI(Advanced eXtensible Interface)是一种总线协议,该协议是ARM公司提出的AMBA(Advanced Microcontroller Bus ...