【项目背景】
随着大语言模型(LLM)的发展以及随之应运而生的MCP(Model Context Protocol,模型上下文协议) ,让我们能用更方便的方式与最新科技力进行交互。为了学习MCP的应用,以及使用工具更好地服务人类生活,故进行此项目的学习开发。
【项目设计】
本项目旨在通过技术手段,为传统赋予智能互动功能。我们将采用行空板M10、电池扩展板以及I2C模块进行硬件搭建,并结合大语言模型(DeepSeek V3)和讯飞语音合成功能,实现一个智能系统,解决到哪里去和吃什么的哲学问题。通过结合高德地图提供的MCP能力,实现美食自动自动获取,随机选择的功能。
【项目步骤】
一、硬件
行空板M10、行空板M10扩展板组合、免驱动喇叭
二、软件
1. python环境的安装
因为行空板默认的Python版本为 3.7.3,不支持MCP库的安装。这里使用Miniconda升级python环境。
在这里找到https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/清华源地址。下载相应的版本。
我这里选择的是:
`https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-py312_25.5.1-0-Linux-aarch64.sh`
需要注意的是我们应该选择 `Linux-aarch64`架构的版本。
可以直接在 bash shell里下载:
`wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-py312_25.5.1-0-Linux-aarch64.sh`
安装 Miniconda
赋予安装脚本执行权限
- chmod +x Miniconda3-py312_25.5.1-0-Linux-aarch64.sh
运行安装脚本
- ./Miniconda3-py312_25.5.1-0-Linux-aarch64.sh
按照提示完成安装。建议安装路径为用户主目录下的 miniconda3 文件夹。
初始化 Conda
安装完成后,初始化 Conda 以确保其在终端中可用:
- source ~/.bashrc
然后关闭 shell 重新打开即可。

我们可以看到默认shell 环境的python版本是 `3.12.11`了。
配置清华镜像源
为了加快 Conda 和 Python 包的下载速度,可以将默认的镜像源更换为清华大学开源镜像站。
更换 Conda 下载源为清华源
备份原有 Conda 配置文件
- cp ~/.condarc ~/.condarc.bak
编辑或创建 .condarc 文件
使用您喜欢的文本编辑器打开 .condarc 文件:
- nano ~/.condarc
将以下内容粘贴到文件中:
- channels:
- - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
- - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
- - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r/
- - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/pro/
- - defaults
- show_channel_urls: true
更新 Conda
运行以下命令以确保 Conda 使用最新配置:
- conda clean -i
- conda update -n base -c defaults conda
设置 Python 包下载源为清华源
创建或编辑 pip 配置文件
mkdir -p ~/.pip
nano ~/.pip/pip.conf
添加豆瓣像源
在配置文件中添加以下内容:
- [global]
- index-url = https://pypi.doubanio.com/simple
mcp要求python≥3.10,我们这里使用比较稳定的3.12版本。
2. 功能简介
本程序的主要功能是使用本地python客户端,通过大语言模型调用在线MCP服务,借助MCP的协议标准,实现更复杂的功能。
本次使用的 MCP 服务是:高德地图的 amap-amap-sse MCP服务。它主要提供以下功能:
为了能正常使用它的服务,我们可以通过它的官方网站注册获取API KEY

官网已经有完整的教程,这里不再赘述。
3. 大语言模型选择
为了更好地结合调用,我们还需要一个LLM 接口。通过第三方的 硅基流动 服务可以完成满血版ds的使用:
首先,注册并登录硅基流动官网。现在硅基流动有活动,用我的邀请链接注册 (https://cloud.siliconflow.cn/i/NxHF7Q41),咱俩都能白嫖14块钱额度,高频使用2周到1个月没问题(真不是广告)。
登录后直接进入【模型广场】,别管排在首位的模型,看向最左边导航栏,找到【API密钥】点进去。

模型我们就选择:
`deepseek-ai/DeepSeek-V3`
4. 部分实现讲解
我们可以看到,程序能实现MCP工具的调用,主要还是通过提示词进行:
在这里替换你自己的key

另外,还需在这里,修改amap mcp的 api 地址,后面的key就是你在高德地图上申请的key。
下载的 sequentialthinking MCP服务,其实也是MCP推荐服务,但是在本项目中,一直有问题,就没有引用了,是一大遗憾。有兴趣的同学去搜索下,就知道它的强大了。

tkinter图像化界面使用了模板的方式,更有人情味。

语音识别使用讯飞语音,按住行空板的A键开始录音,放开A键录音结束。

完成代码如下:
import json
import asyncio
import re
import sys
from typing import Optional
from contextlib import AsyncExitStack
import logging
from mcp import ClientSession, StdioServerParameters
from mcp.client.sse import sse_client
from dotenv import load_dotenv
from openai import AsyncOpenAI, OpenAI
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Dict, List, Optional, Any
import requests
from PIL import Image, ImageTk
import io
import random
import threading
from dataclasses import dataclass
from enum import Enum
import signal
import warnings
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 忽略一些可能的警告
warnings.filterwarnings("ignore", category=DeprecationWarning)
@dataclass
class POIData:
name: str
address: str
location: str
distance: Optional[str] = None
phone: Optional[str] = None
image_url: Optional[str] = None
category: Optional[str] = None
food_list = []
class TkinterFoodGameTemplate:
"""今天吃什么小游戏的Tkinter模板"""
def __init__(self, food_data: List[POIData]):
self.food_data = food_data
self.current_food = None
self.root = None
self.food_image_label = None
self.food_info_label = None
self.photo = None # 添加这个属性来保持图片引用
self.setup_ui()
def setup_ui(self):
"""设置用户界面"""
self.root = tk.Tk()
self.root.title("今天吃什么?")
self.root.geometry("240x320")
self.root.resizable(False, False)
# 设置背景色
self.root.configure(bg='#f0f0f0')
# 添加窗口关闭事件处理
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
# 标题
title_label = tk.Label(
self.root,
text="今天吃什么?",
font=("Arial", 16, "bold"),
bg='#f0f0f0',
fg='#333333'
)
title_label.pack(pady=10)
# 图片显示区域
self.food_image_label = tk.Label(
self.root,
bg='#ffffff',
width=160,
height=120,
relief='solid',
borderwidth=1
)
self.food_image_label.pack(pady=5)
# 餐厅信息显示区域
self.food_info_label = tk.Label(
self.root,
text="点击按钮开始选择!",
font=("Arial", 10),
bg='#f0f0f0',
fg='#666666',
wraplength=200,
justify='center'
)
self.food_info_label.pack(pady=10)
# 按钮区域
button_frame = tk.Frame(self.root, bg='#f0f0f0')
button_frame.pack(pady=10)
# 开始选择按钮
self.start_button = tk.Button(
button_frame,
text="开始选择",
command=self.start_selection,
bg='#4CAF50',
fg='white',
font=("Arial", 12, "bold"),
padx=20,
pady=5,
relief='flat'
)
self.start_button.pack(side='left', padx=5)
# 重新选择按钮
self.reselect_button = tk.Button(
button_frame,
text="重新选择",
command=self.reselect_food,
bg='#FF9800',
fg='white',
font=("Arial", 12, "bold"),
padx=20,
pady=5,
relief='flat'
)
self.reselect_button.pack(side='left', padx=5)
# 详情按钮
self.detail_button = tk.Button(
button_frame,
text="详情",
command=self.show_detail,
bg='#2196F3',
fg='white',
font=("Arial", 12, "bold"),
padx=20,
pady=5,
relief='flat'
)
self.detail_button.pack(side='left', padx=5)
# 加载默认图片
self.load_default_image()
def on_closing(self):
"""窗口关闭时的处理"""
try:
self.root.quit()
self.root.destroy()
except:
pass
def load_default_image(self):
"""加载默认图片"""
try:
# 创建一个默认的占位图片
default_image = Image.new('RGB', (160, 120), color='#ddd')
self.photo = ImageTk.PhotoImage(default_image)
self.food_image_label.configure(image=self.photo)
except Exception as e:
logger.error(f"加载默认图片失败: {str(e)}")
def load_food_image(self, image_url: str):
"""加载餐厅图片"""
try:
if image_url and image_url.startswith('http'):
# 在新线程中加载图片以避免阻塞UI
threading.Thread(
target=self._load_image_thread,
args=(image_url,),
daemon=True
).start()
else:
self.load_default_image()
except Exception as e:
logger.error(f"加载图片失败: {str(e)}")
self.load_default_image()
def _load_image_thread(self, image_url: str):
"""在线程中加载图片"""
try:
response = requests.get(image_url, timeout=10)
if response.status_code == 200:
image = Image.open(io.BytesIO(response.content))
image = image.resize((150, 100), Image.Resampling.LANCZOS)
photo = ImageTk.PhotoImage(image)
# 在主线程中更新UI
if self.root and self.root.winfo_exists():
self.root.after(0, lambda: self._update_image(photo))
else:
if self.root and self.root.winfo_exists():
self.root.after(0, self.load_default_image)
except Exception as e:
logger.error(f"网络加载图片失败: {str(e)}")
if self.root and self.root.winfo_exists():
self.root.after(0, self.load_default_image)
def _update_image(self, photo):
"""更新图片显示"""
try:
if self.food_image_label and self.food_image_label.winfo_exists():
self.photo = photo
self.food_image_label.configure(image=self.photo)
except Exception as e:
logger.error(f"更新图片失败: {str(e)}")
def start_selection(self):
"""开始选择食物"""
if not self.food_data:
messagebox.showwarning("警告", "没有找到附近的餐厅数据!")
return
# 随机选择一个餐厅
self.current_food = random.choice(self.food_data)
# 更新显示
self.update_food_display()
# 添加选择动画效果
self.animate_selection()
def animate_selection(self):
"""选择动画效果"""
try:
if not self.food_info_label or not self.food_info_label.winfo_exists():
return
original_color = self.food_info_label.cget('bg')
def flash(count=0):
try:
if count < 6 and self.food_info_label and self.food_info_label.winfo_exists():
color = '#FFE082' if count % 2 == 0 else original_color
self.food_info_label.configure(bg=color)
if self.root and self.root.winfo_exists():
self.root.after(100, lambda: flash(count + 1))
elif self.food_info_label and self.food_info_label.winfo_exists():
self.food_info_label.configure(bg=original_color)
except Exception as e:
logger.error(f"动画失败: {str(e)}")
flash()
except Exception as e:
logger.error(f"动画初始化失败: {str(e)}")
def update_food_display(self):
"""更新食物显示"""
if self.current_food and self.food_info_label:
try:
info_text = f"{self.current_food.name}\n{self.current_food.address}"
if self.current_food.distance:
info_text += f"\n距离: {self.current_food.distance}米"
self.food_info_label.configure(text=info_text)
# 加载图片
self.load_food_image(self.current_food.image_url)
except Exception as e:
logger.error(f"更新显示失败: {str(e)}")
def reselect_food(self):
"""重新选择食物"""
self.start_selection()
def show_detail(self):
"""显示详细信息"""
if not self.current_food:
messagebox.showinfo("提示", "请先选择一个餐厅!")
return
try:
detail_text = f"餐厅名称: {self.current_food.name}\n"
detail_text += f"地址: {self.current_food.address}\n"
detail_text += f"位置: {self.current_food.location}\n"
if self.current_food.distance:
detail_text += f"距离: {self.current_food.distance}米\n"
if self.current_food.phone:
detail_text += f"电话: {self.current_food.phone}\n"
if self.current_food.category:
detail_text += f"类别: {self.current_food.category}\n"
messagebox.showinfo("餐厅详情", detail_text)
except Exception as e:
logger.error(f"显示详情失败: {str(e)}")
messagebox.showerror("错误", "显示详情时发生错误")
def run(self):
"""运行程序"""
try:
self.root.mainloop()
except Exception as e:
logger.error(f"GUI运行失败: {str(e)}")
finally:
self.cleanup()
def cleanup(self):
"""清理资源"""
try:
if self.root:
self.root.quit()
self.root.destroy()
except:
pass
def format_tools_for_llm(tool) -> str:
"""对tool进行格式化
Returns:
格式化之后的tool描述
"""
args_desc = []
if "properties" in tool.inputSchema:
for param_name, param_info in tool.inputSchema["properties"].items():
arg_desc = (
f"- {param_name}: {param_info.get('description', 'No description')}"
)
if param_name in tool.inputSchema.get("required", []):
arg_desc += " (required)"
args_desc.append(arg_desc)
return f"Tool: {tool.name}\nDescription: {tool.description}\nArguments:\n{chr(10).join(args_desc)}"
class Client:
def __init__(self):
self._exit_stack: Optional[AsyncExitStack] = None
self.session: Optional[ClientSession] = None
self._lock = asyncio.Lock()
self.is_connected = False
self.client = AsyncOpenAI(
base_url="https://api.siliconflow.cn",
api_key="sk-kxwsrzianq*******wymrfp",
)
self.model = "deepseek-ai/DeepSeek-V3"
self.messages = []
self.tools = {}
self._cleanup_done = False
async def connect_server(self, server_config):
"""连接到服务器"""
async with self._lock:
try:
if self.is_connected:
logger.info("已经连接到服务器")
return
url = server_config["mcpServers"]["amap-amap-sse"]["url"]
print(f"尝试连接到: {url}")
self._exit_stack = AsyncExitStack()
# 使用超时和重试机制
max_retries = 3
for attempt in range(max_retries):
try:
# 1. 进入 SSE 上下文
sse_cm = sse_client(url)
streams = await asyncio.wait_for(
self._exit_stack.enter_async_context(sse_cm),
timeout=30.0
)
print("SSE 流已获取。")
# 2. 进入 Session 上下文
session_cm = ClientSession(streams[0], streams[1])
self.session = await asyncio.wait_for(
self._exit_stack.enter_async_context(session_cm),
timeout=30.0
)
print("ClientSession 已创建。")
# 3. 初始化 Session
await asyncio.wait_for(self.session.initialize(), timeout=30.0)
print("Session 已初始化。")
# 4. 获取工具列表
response = await asyncio.wait_for(self.session.list_tools(), timeout=30.0)
self.tools = {tool.name: tool for tool in response.tools}
print(f"成功获取 {len(self.tools)} 个工具:")
for name, tool in self.tools.items():
print(f" - {name}: {tool.description[:50]}...")
self.is_connected = True
break
except asyncio.TimeoutError:
print(f"连接超时,尝试重试 ({attempt + 1}/{max_retries})")
if attempt == max_retries - 1:
raise
await asyncio.sleep(2 ** attempt) # 指数退避
except Exception as e:
print(f"连接异常: {e}")
if attempt == max_retries - 1:
raise
await asyncio.sleep(2 ** attempt)
# 设置系统提示
await self._setup_system_prompt()
print("连接成功并准备就绪。")
except Exception as e:
print(f"连接失败: {e}")
await self._cleanup_connection()
raise
async def _setup_system_prompt(self):
"""设置系统提示"""
if not self.tools:
return
tools_description = "\n".join([format_tools_for_llm(tool) for tool in self.tools.values()])
system_prompt = (
"You are a helpful assistant with access to these tools:\n\n"
f"{tools_description}\n"
"Choose the appropriate tool based on the user's question. "
"If no tool is needed, reply directly.\n\n"
"IMPORTANT: When you need to use a tool, you must ONLY respond with "
"the exact JSON object format below, nothing else:\n"
"{\n"
' "tool": "tool-name",\n'
' "arguments": {\n'
' "argument-name": "value"\n'
" }\n"
"}\n\n"
'"```json" is not allowed'
"After receiving a tool's response:\n"
"1. Transform the raw data into a natural, conversational response\n"
"2. Keep responses concise but informative\n"
"3. Focus on the most relevant information\n"
"4. Use appropriate context from the user's question\n"
"5. Avoid simply repeating the raw data\n\n"
"Please use only the tools that are explicitly defined above."
)
self.messages.append({"role": "system", "content": system_prompt})
async def _cleanup_connection(self):
"""清理连接资源"""
if self._cleanup_done:
return
try:
if self._exit_stack:
await self._exit_stack.aclose()
except Exception as e:
logger.error(f"清理连接资源失败: {e}")
finally:
self._exit_stack = None
self.session = None
self.is_connected = False
self._cleanup_done = True
async def disconnect(self):
"""断开连接"""
async with self._lock:
print("正在断开连接...")
await self._cleanup_connection()
print("连接已断开。")
async def chat(self, prompt, role="user"):
"""与LLM进行交互"""
try:
self.messages.append({"role": role, "content": prompt})
# 使用超时机制
response = await asyncio.wait_for(
self.client.chat.completions.create(
model=self.model,
messages=self.messages,
),
timeout=60.0
)
llm_response = response.choices[0].message.content
return llm_response
except asyncio.TimeoutError:
error_msg = "LLM API 调用超时"
logger.error(error_msg)
return error_msg
except Exception as e:
error_msg = f"LLM API 调用失败: {str(e)}"
logger.error(error_msg)
return error_msg
async def execute_tool(self, llm_response: str):
"""执行工具调用"""
try:
# 解析工具调用
pattern = r"```json\n(.*?)\n?```"
match = re.search(pattern, llm_response, re.DOTALL)
if match:
llm_response = match.group(1)
tool_call = json.loads(llm_response)
if "tool" in tool_call and "arguments" in tool_call:
tool_name = tool_call["tool"]
tool_args = tool_call["arguments"]
# 检查工具是否存在
if tool_name not in self.tools:
return f"未找到工具: {tool_name}"
print(f"[提示]:正在调用工具 {tool_name}")
# 调用工具(使用超时)
result = await asyncio.wait_for(
self.session.call_tool(tool_name, tool_args),
timeout=60.0
)
# 处理美食搜索结果
if tool_name == 'maps_around_search':
await self._process_food_search_result(result)
return f"Tool execution result: {result}"
return llm_response
except asyncio.TimeoutError:
error_msg = "工具调用超时"
logger.error(error_msg)
return error_msg
except json.JSONDecodeError:
return llm_response
except Exception as e:
error_msg = f"工具执行失败: {str(e)}"
logger.error(error_msg)
return error_msg
async def _process_food_search_result(self, result):
"""处理美食搜索结果"""
global food_list
food_list = []
try:
result_data = json.loads(result.model_dump()['content'][0]['text'])
if "pois" in result_data:
logger.info(f"找到 {len(result_data['pois'])} 个餐厅")
for poi in result_data["pois"]:
food_data = POIData(
name=poi.get("name", "未知餐厅"),
address=poi.get("address", "地址未知"),
location=poi.get("location", ""),
distance=poi.get("distance", ""),
phone=poi.get("tel", ""),
image_url=poi.get("photo", ""),
category=poi.get("typecode", "")
)
food_list.append(food_data)
else:
logger.warning("未找到餐厅数据")
except Exception as e:
logger.error(f"处理搜索结果失败: {e}")
async def chat_loop(self):
"""运行交互式聊天循环"""
print("MCP 客户端启动")
print("输入 /bye 退出")
try:
while True:
try:
prompt = input(">>> ").strip()
if "/bye" in prompt.lower():
break
if prompt == '':
prompt = "我在温州五马街,有什么吃的推荐?"
response = await self.chat(prompt)
if not response:
continue
self.messages.append({"role": "assistant", "content": response})
result = await self.execute_tool(response)
while result != response:
response = await self.chat(result, "system")
if not response:
break
self.messages.append({"role": "assistant", "content": response})
result = await self.execute_tool(response)
print(response)
# 启动GUI(如果有美食数据)
await self._launch_food_game()
except KeyboardInterrupt:
print("\n用户中断操作")
break
except Exception as e:
print(f"聊天循环出错: {e}")
continue
except Exception as e:
print(f"聊天循环异常: {e}")
finally:
print("聊天循环结束")
async def _launch_food_game(self):
"""启动美食游戏"""
global food_list
# 创建模拟数据(如果没有真实数据)
if not food_list:
logger.warning("未找到真实美食数据,使用模拟数据")
food_list = [
POIData("知春路麻辣烫", "知春路地铁站附近", "116.123,39.456", "100", "010-12345678",
"https://via.placeholder.com/150x100/FF6B6B/FFFFFF?text=麻辣烫", "餐厅"),
POIData("老北京炸酱面", "知春路大厦", "116.124,39.457", "200", "010-87654321",
"https://via.placeholder.com/150x100/4ECDC4/FFFFFF?text=炸酱面", "餐厅"),
POIData("川菜馆", "知春路购物中心", "116.125,39.458", "300", "010-11111111",
"https://via.placeholder.com/150x100/45B7D1/FFFFFF?text=川菜", "餐厅"),
]
# 在新线程中运行GUI,避免阻塞异步循环
def run_gui():
try:
logger.info(f"启动游戏窗口,共有 {len(food_list)} 个餐厅选项")
game = TkinterFoodGameTemplate(food_list)
game.run()
except Exception as e:
logger.error(f"GUI运行失败: {e}")
# 使用线程运行GUI
gui_thread = threading.Thread(target=run_gui, daemon=True)
gui_thread.start()
# 等待GUI线程完成(非阻塞)
while gui_thread.is_alive():
await asyncio.sleep(0.1)
def load_server_config(config_file):
"""加载服务器配置"""
try:
with open(config_file, 'r', encoding='utf-8') as f:
return json.load(f)
except FileNotFoundError:
logger.error(f"配置文件 {config_file} 不存在")
raise
except json.JSONDecodeError as e:
logger.error(f"配置文件解析失败: {e}")
raise
def setup_signal_handlers():
"""设置信号处理器"""
def signal_handler(sig, frame):
print(f"\n收到信号 {sig},正在退出...")
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
async def main():
"""主函数"""
client = None
try:
# 设置信号处理器
setup_signal_handlers()
# 加载配置
# server_config = load_server_config("servers_config.json")
server_config = {
"mcpServers": {
"amap-amap-sse": {
"url": "https://mcp.amap.com/sse?key=******"
},
"sequentialthinking": {
"type": "sse",
"url": "https://mcp.api-inference.modelscope.net/********/sse"
}
}
}
# 创建客户端
client = Client()
# 连接服务器
await client.connect_server(server_config)
# 运行聊天循环
await client.chat_loop()
except KeyboardInterrupt:
print("\n用户中断操作")
except FileNotFoundError:
print("配置文件不存在,请检查 servers_config.json 文件")
except Exception as e:
print(f"主程序发生错误: {type(e).__name__}: {e}")
import traceback
traceback.print_exc()
finally:
# 清理资源
if client:
try:
print("\n正在关闭客户端...")
await client.disconnect()
print("客户端已关闭。")
except Exception as e:
print(f"关闭客户端时出错: {e}")
if __name__ == '__main__':
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n程序被用户中断")
except Exception as e:
print(f"程序异常退出: {e}")
import traceback
traceback.print_exc()
代码比较长,是因为我把 生成的窗口代码都放在一起了。
5. 运行截图:
以下是在 SSH 环境下 运行的代码提示。

完成效果图。


四、项目总结
本项目主要实现以下功能:
- 1. 问题解决:本项目的主要目的是,当我们出去旅游不知道当前位置有什么好吃的时候,可以直接通过大语言的MCP服务,一键找到美食,成功地解决了有什么好吃的,和吃什么的问题;
- 2. 使用 python 客户端直接调用MCP服务,为实现MCP自由提供了很好的实例;
- 3. 通过创建有趣的tkinter选择界面,增强了交互,也再次学习了python图形界面的开发,特别是界面上的图片异步下载更新,为以后的成长提供了很好代码修炼资源,虽然里面的大部分代码是由AI生成的,但也有人工参与。
- 4. 本项目只使用了行空板扩展板的随身电池功能,还没测试扩展板丰富的接口能力。这个将在下一篇文章中展开,敬请期待。
五、后记
虽然有了LLM AI编程的助力,但调试真的是个大问题,花了大量的token还是回到了最初的版本,没有用上另外一个 MCP始终心有不甘!但暑假今年7月6号才开始啊,期末忙成狗。只好先做个demo完成任务先。
木子哦2025.07.01
蹲一个后续