进阶示例:构建全能出行助手(集成模型/记忆/沙箱/网关/高德mcp)
场景概述
在基础示例中,我们已创建一个简易对话智能体,并将其托管部署至云上AgentArts环境。但在真实生产环境下,企业级智能体需要具备持久化记忆、安全沙箱执行、外部系统无缝对接等能力。
本示例将基于LangGraph框架构建一个出行规划智能体,核心实现agent.py的TravelAgent类。该类封装LangGraph ReAct工作流(agent 节点完成 LLM 推理,tools 节点负责工具执行)、MCP工具动态发现、云端记忆持久化、代码沙箱调用等特性;同时集成AgentArts平台的Gateway工具调用、代码沙箱执行、云端记忆持久化三大核心能力,部署于AgentArts运行时,对外提供多轮对话式出行规划服务。
用户输入自然语言指令,例如 “查杭州天气”、“帮我算两天行程预算”,Agent 会自主选择工具与调用顺序,最终输出结构化回复。
本示例将构建 “全能出行助手” 智能体,重点演示如下能力:
- 记忆库(Memory):对接AgentArts云端记忆库组件,提供长短期记忆能力,完成跨会话上下文持久化。用户开启新会话时,可加载历史对话记录,接续完成出行规划。
- 网关(Gateway):作为Agent调用外部工具的统一入口,依托标准MC 协议转换能力对接外部工具;服务启动时动态拉取全部工具定义。示例接入高德地图15个MCP工具,支持天气查询、地理编码、多模式路径规划(驾车 / 步行 / 骑行 / 公交)、周边搜索、距离测量等功能。
- 工具(CodeInterpreter):依托平台完全隔离的容器环境,创建安全沙箱会话,执行LLM生成的Python代码并返回运算结果。在出行规划场景下,可完成预算核算(门票 + 住宿 + 餐饮 + 交通汇总)、多套方案费用对比、行程增量调整等工作。
- 身份认证(Identity):与网关协同,完成工具链路的标准化鉴权,保障外部工具调用安全。
架构图与工作流程
整体架构图

工作流程

环境准备
- 操作系统:Ubuntu ARM64,服务器可访问公网。
需使用ARM64系统制作的Agent镜像,使用X86系统制作的镜像在调用智能体运行时时会调用失败。
可以使用华为云ECS服务购买服务器,购买时需要选择鲲鹏架构(ARM64),操作系统选择Ubuntu。

- 安装Python:请确保Python 3.10及以上版本已安装。
大多数Linux发行版(如Ubuntu)都预装了Python,您可以先通过python3 --version检查。如未安装,可以使用如下命令安装:
sudo apt update sudo apt install python3
- 安装Docker:请确保Docker 18.06及以上版本已安装。如未安装,可以使用如下命令安装:
# 查询 Docker 版本 docker --version # 安装Docker sudo apt update sudo apt install docker.io
安装完成后,建议立即配置国内镜像加速器。在后续执行agentarts launch构建镜像时,Docker需要拉取python:3.10-slim基础镜像,默认从docker.io官方仓库下载,可能超时失败。提前配置加速器可避免此问题。
执行以下命令创建Docker配置目录:
sudo mkdir -p /etc/docker
执行以下命令配置国内镜像源(直接复制并回车执行):
sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors":[ "https://docker.m.daocloud.net", "https://dockerproxy.net", "https://mirror.baidubce.com" ] } EOF依次执行以下命令重启Docker服务,使配置生效:
sudo systemctl daemon-reload sudo systemctl restart docker
华为云SWR基础版不支持OCI镜像格式,如果您使用的是Docker 27及以上版本,并且需要处理OCI镜像,可以通过设置环境变量来关闭OCI支持。
export DOCKER_BUILDKIT=0 # 或者 export BUILDKIT_USE_OCI_MEDIA_TYPES=0
- 安装SDK。
- 在Linux系统中创建项目目录并进入。
mkdir ./test2 cd ./test2
- 执行以下命令安装SDK(建议在Python虚拟环境中安装,以避免与系统包产生冲突)。
# 安装依赖包 apt install python3.12-venv # 创建并激活虚拟环境 (linux) python3 -m venv venv source venv/bin/activate # 安装sdk pip install agentarts-sdk
- 在Linux系统中创建项目目录并进入。
- 执行以下命令配置华为云凭证,获取华为云凭证请参考认证鉴权。
export HUAWEICLOUD_SDK_AK="your-access-key" export HUAWEICLOUD_SDK_SK="your-secret-key"
智能体开发全流程
本示例涉及的代码样例可通过代码示例全量下载,也可直接从帮助文档中复制使用。
步骤一:平台组件创建与配置
在AgentArts平台依次创建记忆库、沙箱工具和网关,并获取后续代码接入所需的各项配置信息。
- 在AgentArts平台创建记忆库。
- 登录AgentArts智能体平台。
- 在左侧导航栏选择“开发中心 > 组件库 ”,进入“记忆库”界面。
- 单击“创建记忆库”。填写记忆库名称、描述;勾选“私网访问”、“公网访问”;勾选全部的“长期记忆提取策略”,其余参数使用默认配置。 图1 创建记忆库
图2 配置记忆策略
- 记忆库创建完成后,单击记忆库名称,进入“基本信息”界面,获取记忆库ID。 图3 获取记忆库ID
- 在“API Key”处单击“更新”。 图4 更新API Key
- 弹框中单击“确定”后,复制或下载API Key。 图5 复制或下载API Key
- 在AgentArts平台创建沙箱工具。
- 在AgentArts平台左侧导航栏中选择“开发中心 > 组件库”,并进入“沙箱工具”页面,单击“创建代码解释器”。
- 填写代码解释器相关配置。参考下表进行配置。
表1 代码解释器配置说明 参数
说明
名称
可自定义。
描述
可自定义。
委托(可选)
(可选)授予的代理权限或代理功能,允许代表智能体与外部系统进行通信和交互。
可不填写,如需手动创建,请单击“创建委托”并参考如下配置创建。
- 委托名称:自定义。
- 信任主体类型:选择“云服务”。
- 云服务:搜索service.WorkloadSandboxMetadata。
- 其余参数使用默认值。
图6 创建委托
入站身份认证
选择“API Key认证”。
API Key名称
可自定义。
日志记录
开启。
出网网络配置
选择“公网访问”。
图7 创建沙箱工具
- 配置完成后,单击“立即创建”创建沙箱。
单击沙箱工具,进入“配置信息”页面,获取域名。
在“配置信息”页面,单击URN链接,在新页面获取沙箱API Key。
图8 获取沙箱工具名称
图9 获取沙箱域名
图10 获取沙箱API Key
- 在AgentArts平台创建网关,配置高德MCP服务。
- 高德MCP如下,使用前,请参考高德开放平台文档,创建获取API Key。
{ "mcpServers": { "amap-maps-streamableHTTP": { "url": "https://mcp.amap.com/mcp?key=您在高德官网上申请的key" } } } - 在AgentArts平台左侧导航栏中选择“开发中心 > 组件库”,并进入“网关”页面,单击“创建网关”。
- 填写网关基础信息并配置权限与身份认证。参考下表进行配置。
表2 网关基础信息与权限身份认证配置 参数
说明
名称
可自定义。
描述(可选)
网关的描述信息,可自定义。
MCP 版本
2025-03-26
委托
使用平台的默认值。
入站身份认证
选择API Key认证。
API Key名称
可自定义。
日志记录
选择开启。
工具检索
选择开启。
高级配置
选择公网访问,开启会话保持。
图11 填写网关基础信息与权限身份认证
- 单击“创建Target”,配置高德MCP服务。
表3 高德Target配置 参数
说明
名称
填写为gaodemap。
描述(可选)
可自定义。
类型
选择“MCP”,使用现成的高德MCP服务,不需要使用直接对接外部REST API的方式。
传输方式
选择“Streamable HTTP”,与高德MCP的调用方式保持一致。
MCP地址
填写:https://mcp.amap.com/mcp
出站身份认证
选择“API Key”,单击“创建出站身份”,参数配置如下:
- 身份名称:填写为gaodemcp。
- 认证类型:选择“API Key”。
- API Key的值:填写高德开发平台中创建的API Key。
出站身份创建完成后,返回“创建Target”页面,选择已创建的出站身份。
- 位置:选择“查询参数”。
- 参数名称:填写为key。
- 前缀:不填。
图12 创建出站身份
图13 配置出站身份认证
- 配置完成后,单击“确定”创建Target。Target创建完成后,回到“创建网关”页面,高级配置选择“公网访问”后,单击“创建网关”。
- 网关创建完成后,回到网关列表页面。单击网关名称,记录网关URL及网关的API Key(注意是网关的API Key,不是高德mcp的key)。 图14 网关列表
- 调测网关。
MCP网关配置完成后,建议在“调测”页面进行一次测试,验证MCP工具是否可以正常调用。在网关详情页面单击“调测”标签,确认能够正常返回结果后再进行后续开发。
图15 调测网关
- 在网关详情页面获取网关URL。 图16 获取网关URL
- 在网关详情页面,单击URN链接。进入Agent Identity页面,获取网关的API Key。 图17 单击URN链接
图18 获取网关API Key
- 高德MCP如下,使用前,请参考高德开放平台文档,创建获取API Key。
步骤二:编写Agent核心逻辑
使用LangGraph框架编写智能体的核心对话逻辑,并集成记忆库、MCP 网关、沙箱工具与华为云MaaS模型。
- 执行如下命令安装langchain、langgraph、langchain-openai。
pip install -U langchain langgraph langchain-openai
- 创建.env环境变量配置文件。
# ========================================== # 1. 模型配置 (对接华为云MaaS模型) # ========================================== MODEL_NAME=deepseek-v4-flash MODEL_URL=https://api.modelarts-maas.com/openai/v1 MODEL_API_KEY=替换为真实的模型API Key MODEL_TYPE=openai # ========================================== # 2. AgentArts 记忆库配置 # ========================================== MEMORY_SPACE_ID=替换为记忆库 ID HUAWEICLOUD_SDK_MEMORY_API_KEY=替换为记忆库的API Key HUAWEICLOUD_SDK_REGION=cn-southwest-2 # ========================================== # 3. 网关配置 # ========================================== # 网关地址 GATEWAY_ENDPOINT=替换为网关的URL # 网关的 apikey GATEWAY_INBOUND_TOKEN=替换为网关的API Key # ========================================== # 4. 沙箱配置 # ========================================== SANDBOX_NAME=在AgentArts平台中创建的沙箱名称 AGENTARTS_CODEINTERPRETER_DATA_ENDPOINT="http://您的沙箱域名" HUAWEICLOUD_SDK_CODE_INTERPRETER_API_KEY="您的沙箱 API Key" # ========================================== # 5. Agent 全局配置 # ========================================== MAX_ITERATIONS=50 SESSION_TTL=3600 # 本地测试启动 HTTP Server 时绑定的端口 AGENT_RUN_PORT=8080
获取模型API Key的方法请参考获取华为云MaaS服务模型API Key。
agent.py文件的示例中,使用华为云MaaS服务提供的deepseek-v4-flash模型,如果使用其他模型注意MODEL_NAME的值,需填写为模型接口中model参数的值,可以参考如下方式获取。注意需要选择OpenAI兼容接口。
图19 获取OpenAI兼容接口的model值
- 在本地创建agent.py文件,编辑代码如下。 示例代码中创建了出行助手智能体,并对接记忆库、网关、沙箱、身份认证、华为云MaaS服务提供的模型。
""" ================================================================================ AgentArts LangGraph 智能体核心实现 ================================================================================ 启动时自动从Gateway 拉取 tools/list,动态生成 LangChain Tool 对象。 ================================================================================ """ import os import uuid import json import re from typing import List, Optional, Dict, Any, Type import requests import urllib3 from dotenv import load_dotenv from pydantic import create_model, Field urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) from langgraph.graph import StateGraph, END from langgraph.prebuilt import ToolNode from langgraph.graph import MessagesState from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage, AIMessage, ToolMessage from langchain_core.tools import StructuredTool from langchain_openai import ChatOpenAI try: from agentarts.sdk.memory import MemoryClient from agentarts.sdk.memory.inner.config import TextMessage except ImportError: MemoryClient = None TextMessage = None print("[Warning] 未检测到 agentarts.sdk,将跳过云端记忆同步。") load_dotenv() # ================================================================================ # 常量 # ================================================================================ _MAX_ITERATIONS = 12 _FORCE_FINAL_REPLY_ITERATION = 10 _TOOL_RESULT_MAX_LENGTH = 8000 _HISTORY_K_MESSAGES = 20 _REQUEST_TIMEOUT = 30 _ACTOR_ID = "user_123" _ASSISTANT_ID = "travel_agent" _TOOL_CALL_TAG = "<tool_call>" _DEFAULT_MODEL_NAME = "deepseek-v4-flash" _DEFAULT_MODEL_URL = "https://api.modelarts-maas.com/openai/v1" _DEFAULT_REGION = "cn-southwest-2" _DEFAULT_MAX_ITERATIONS = 50 _SYSTEM_PROMPT = ( "你是一个专业的出行规划助手,具备以下能力:\n\n" "【地图服务 - 高德MCP】\n" "- 天气查询:帮用户判断出行日期是否合适\n" "- 地理编码:将地址转为坐标\n" "- 路径规划:计算驾车/步行/骑行/公交路线\n" "- 周边搜索:查找餐厅、酒店、景点等\n\n" "【代码沙箱】\n" "- 仅在用户明确要求计算费用/预算/总花费时使用\n" "- 不适用于:查位置、查路线、查天气、展示信息\n\n" "【云端记忆】\n" "- 记住用户提到的城市、日期、偏好\n" "- 跨轮次累积规划,支持增量调整\n\n" "工作原则:\n" "1. 规划出行时,先查天气判断可行性\n" "2. **何时调用 execute_python**:只有当用户明确要求计算费用、预算、总价、多方案费用对比时才调用。" "查位置、查路线、查天气、展示距离等任务直接用工具返回的结果回答,**不要调用 execute_python**。\n" " - 对于不确定的价格(如门票、住宿、餐饮),用合理估计值\n" " - 在 Python 代码中定义这些变量,用代码做加法/乘法计算总费用\n" " - 禁止直接给出估算数字,必须通过沙箱计算\n" "3. 记住用户之前提到的所有信息,避免重复询问\n" "4. 多轮对话中,基于已有上下文做增量调整\n" "5. 路径规划时,尽量同时查询所有出行方式(驾车、步行、骑行、公交),给用户完整的对比" ) # ================================================================================ # 消息清洗(防止 ModelArts 81001 错误) # ================================================================================ def clean_messages_for_llm(messages: List[BaseMessage]) -> List[BaseMessage]: cleaned = [] expecting_tool_results = False for msg in messages: if isinstance(msg, AIMessage) and getattr(msg, "tool_calls", None): cleaned.append(msg) expecting_tool_results = True elif isinstance(msg, ToolMessage): if expecting_tool_results: cleaned.append(msg) else: print("[MaaS Guard] 检测到孤立的 ToolMessage,已自动过滤。") else: cleaned.append(msg) expecting_tool_results = False return cleaned # ================================================================================ # 配置 # ================================================================================ class ModelConfig: def __init__(self): self.name = os.getenv("MODEL_NAME", _DEFAULT_MODEL_NAME) self.url = os.getenv("MODEL_URL", _DEFAULT_MODEL_URL) self.api_key = os.getenv("MODEL_API_KEY") if not self.api_key: raise ValueError("[错误] 未找到 MODEL_API_KEY!请在 .env 文件中进行配置。") class AgentConfig: def __init__(self): self.model = ModelConfig() self.memory_space = os.getenv("MEMORY_SPACE_ID") if not self.memory_space: print("[Warning] 未配置 MEMORY_SPACE_ID,云端记忆库可能无法正常工作。") self.region = os.getenv("HUAWEICLOUD_SDK_REGION", _DEFAULT_REGION) self.max_iterations = int(os.getenv("MAX_ITERATIONS", str(_DEFAULT_MAX_ITERATIONS))) config = AgentConfig() # ================================================================================ # Gateway 交互层(tools/list + tools/call) # ================================================================================ def _mcp_headers() -> Dict[str, str]: return { "Content-Type": "application/json", "Accept": "application/json, text/event-stream", "mcp-session-id": str(uuid.uuid4()), "Authorization": f"Bearer {os.getenv('GATEWAY_INBOUND_TOKEN', '')}" } def _parse_mcp_response(raw_text: str) -> dict: """解析 MCP 响应:兼容 SSE (data: {...}) 和纯 JSON 两种格式""" if "data:" in raw_text: match = re.search(r'\{.*\}', raw_text, re.DOTALL) if match: return json.loads(match.group()) return json.loads(raw_text) def _mcp_request(method: str, params: dict = None) -> dict: """通用 MCP JSON-RPC 请求""" gateway_url = os.getenv("GATEWAY_ENDPOINT") if not gateway_url: raise RuntimeError("未配置 GATEWAY_ENDPOINT") payload = { "jsonrpc": "2.0", "id": str(uuid.uuid4()), "method": method, "params": params or {} } resp = requests.post(gateway_url, headers=_mcp_headers(), json=payload, verify=False, timeout=_REQUEST_TIMEOUT) return _parse_mcp_response(resp.text) def list_mcp_tools() -> List[dict]: """第一步:调 tools/list 拉取 Gateway 上所有工具定义(支持 cursor 分页)""" all_tools = [] cursor = None try: while True: params = {"cursor": cursor} if cursor else {} result = _mcp_request("tools/list", params) result_data = result.get("result", {}) all_tools.extend(result_data.get("tools", [])) cursor = result_data.get("nextCursor") if not cursor: break print(f"[MCP] 从 Gateway 发现 {len(all_tools)} 个工具") return all_tools except Exception as e: print(f"[MCP] tools/list 失败: {e}") return all_tools _TRUNCATION_SUFFIX = "\n...(结果已截断,原始数据过长)" _MIN_TRUNCATION_RATIO = 0.8 def _truncate_safely(text: str, max_length: int) -> str: """安全截断:在 JSON 结构边界处截断,避免破坏数据完整性""" if len(text) <= max_length: return text truncated = text[:max_length] last_brace = max(truncated.rfind('}'), truncated.rfind(']')) if last_brace > max_length * _MIN_TRUNCATION_RATIO: return truncated[:last_brace + 1] + _TRUNCATION_SUFFIX return truncated + _TRUNCATION_SUFFIX def call_mcp_tool(tool_name: str, arguments: Dict[str, Any]) -> str: """第四步:模型决定调某工具后,透传到 MCP tools/call""" try: result = _mcp_request("tools/call", {"name": tool_name, "arguments": arguments}) text = json.dumps(result.get("result", result), ensure_ascii=False) return _truncate_safely(text, _TOOL_RESULT_MAX_LENGTH) except Exception as e: return f"网关调用异常: {e}" # ================================================================================ # 第二步:把 MCP 工具定义动态转成 LangChain StructuredTool # ================================================================================ _JSON_TYPE_MAP = { "string": str, "number": float, "integer": int, "boolean": bool, "array": list, "object": dict, } def _json_schema_to_pydantic(schema: dict, model_name: str = "Args") -> Type: """将 MCP inputSchema (JSON Schema) 转为 Pydantic Model 类""" properties = schema.get("properties", {}) required = set(schema.get("required", [])) fields = {} for param_name, param_info in properties.items(): json_type = param_info.get("type", "string") py_type = _JSON_TYPE_MAP.get(json_type, str) desc = param_info.get("description", "") if param_name in required: fields[param_name] = (py_type, Field(..., description=desc)) else: fields[param_name] = (Optional[py_type], Field(None, description=desc)) return create_model(model_name, **fields) if fields else create_model(model_name) def _make_tool_caller(tool_name: str): """闭包工厂:每个工具绑定自己的 tool_name,防止循环变量捕获问题""" def caller(**kwargs): clean_args = {k: v for k, v in kwargs.items() if v is not None} for k, v in clean_args.items(): if isinstance(v, (int, float)): clean_args[k] = str(v) print(f"\n[Gateway] 调用工具: {tool_name} | 参数: {clean_args}") return call_mcp_tool(tool_name, clean_args) return caller def build_langchain_tool(mcp_tool_def: dict) -> StructuredTool: """将单个 MCP 工具定义转为 LangChain StructuredTool""" name = mcp_tool_def["name"] desc = mcp_tool_def.get("description", "") or f"MCP tool: {name}" schema = mcp_tool_def.get("inputSchema", {}) args_model = _json_schema_to_pydantic(schema, f"{name}_Args") caller = _make_tool_caller(name) return StructuredTool.from_function( func=caller, name=name, description=desc, args_schema=args_model, ) def discover_mcp_tools() -> List[StructuredTool]: """完整的工具发现流程:list -> build""" mcp_tools = list_mcp_tools() return [build_langchain_tool(t) for t in mcp_tools] # ================================================================================ # 本地工具:代码解释器(非 MCP,保持独立定义) # ================================================================================ def _execute_python(code: str) -> str: from agentarts.sdk.tools import code_session print(f"\n[Sandbox] 正在安全沙箱中执行代码...") sandbox_name = os.getenv("SANDBOX_NAME") api_key = os.getenv("SANDBOX_API_KEY") or os.getenv("HUAWEICLOUD_SDK_CODE_INTERPRETER_API_KEY") if not sandbox_name: return "沙箱调用失败: 未在环境变量中配置 SANDBOX_NAME" try: with code_session(config.region, sandbox_name, api_key=api_key) as client: res = client.invoke(operate_type="execute_code", api_key=api_key, arguments={"code": code, "language": "python"}) return json.dumps(res.get("result")) except Exception as e: return f"沙箱执行失败: {e}" execute_python = StructuredTool.from_function( func=_execute_python, name="execute_python", description="在平台沙箱环境中安全执行 Python 代码。Args: code: 待执行的Python代码", args_schema=create_model("execute_python_Args", code=(str, Field(..., description="待执行的Python代码"))) ) # ================================================================================ # LangGraph 工作流 # ================================================================================ class AgentState(MessagesState): iteration: int def should_continue(state: AgentState) -> str: if state.get("iteration", 0) >= _MAX_ITERATIONS: print(f"[Safety] 达到最大迭代次数({_MAX_ITERATIONS}),强制结束") return "end" last_message = state["messages"][-1] if hasattr(last_message, "tool_calls") and last_message.tool_calls: return "continue" return "end" class TravelAgent: def __init__(self): self._llm = ChatOpenAI( model=config.model.name, base_url=config.model.url, api_key=config.model.api_key, temperature=0.1, ) self._mcp_tools = discover_mcp_tools() self._all_tools = self._mcp_tools + [execute_python] print(f"[Agent] 共加载 {len(self._all_tools)} 个工具(MCP: {len(self._mcp_tools)}, 本地: 1)") self.graph = self._build_graph() def _build_graph(self): def model_node(state: AgentState) -> AgentState: messages = state["messages"] current_iteration = state.get("iteration", 0) if current_iteration >= _FORCE_FINAL_REPLY_ITERATION: print("[Safety] 接近最大迭代次数,强制生成最终回复") final_prompt = SystemMessage(content=( "你已达到工具调用次数上限。请基于已有工具返回的结果," "直接给出最终的文字回复,不要再调用任何工具。" "如果信息不足,请如实告知用户已获取的部分结果。" )) safe_messages = clean_messages_for_llm(messages + [final_prompt]) response = self._llm.invoke(safe_messages) else: llm_with_tools = self._llm.bind_tools(self._all_tools) safe_messages = clean_messages_for_llm(messages) response = llm_with_tools.invoke(safe_messages) return {"messages": [response], "iteration": current_iteration + 1} workflow = StateGraph(AgentState) workflow.add_node("agent", model_node) workflow.add_node("tools", ToolNode(self._all_tools)) workflow.set_entry_point("agent") workflow.add_conditional_edges("agent", should_continue, {"continue": "tools", "end": END}) workflow.add_edge("tools", "agent") # 不使用 checkpointer,每轮从云端记忆库加载历史 return workflow.compile() def run(self, user_input: str, session_id: str) -> str: thread_config = {"configurable": {"thread_id": session_id}} self._ensure_session(session_id) history = self._load_cloud_history(session_id) messages = self._build_messages(history, user_input) prev_count = len(messages) - 1 result = self.graph.invoke( {"messages": messages, "iteration": 0}, config=thread_config, recursion_limit=config.max_iterations ) new_messages = result["messages"][prev_count:] self._save_to_cloud_memory(session_id, new_messages) return self._clean_response(result["messages"][-1].content) def _ensure_session(self, session_id: str): """确保云端记忆会话存在""" if MemoryClient is None or not config.memory_space: return try: MemoryClient().create_memory_session( space_id=config.memory_space, id=session_id, actor_id=_ACTOR_ID ) except Exception as e: if "Session id already exists" not in str(e): print(f"[Warning] 云端记忆会话同步失败: {e}") def _build_messages(self, history: List[BaseMessage], user_input: str) -> List[BaseMessage]: """构建送入 LLM 的消息列表:系统提示 + 历史 + 当前输入""" messages = [SystemMessage(content=_SYSTEM_PROMPT)] messages.extend(history) messages.append(HumanMessage(content=user_input)) return messages def _clean_response(self, response: str) -> str: """清理可能幻觉的工具调用标签""" if _TOOL_CALL_TAG in response: return response[:response.index(_TOOL_CALL_TAG)].strip() return response def _load_cloud_history(self, session_id: str) -> List[BaseMessage]: """从云端记忆加载历史消息""" if MemoryClient is None or not config.memory_space: return [] try: client = MemoryClient() response = client.get_last_k_messages( space_id=config.memory_space, session_id=session_id, k=_HISTORY_K_MESSAGES ) msgs = None if isinstance(response, list): msgs = response elif hasattr(response, 'messages'): msgs = response.messages if msgs is None: print(f"[Memory] 无法提取消息列表,{type(response).__name__}") return [] history = [] for m in msgs: role = getattr(m, "role", "") content = getattr(m, "content", "") if not content and hasattr(m, 'parts') and m.parts: part = m.parts[0] content = part.get('text', '') if isinstance(part, dict) else getattr(part, 'text', '') if role == "user" and content: history.append(HumanMessage(content=content)) elif role == "assistant" and content: history.append(AIMessage(content=content)) if history: print(f"[Memory] 从云端加载 {len(history)} 条历史消息") return history except Exception as e: print(f"[Memory] 加载历史失败: {e}") return [] def _save_to_cloud_memory(self, session_id: str, new_messages: List[BaseMessage]): """增量同步:只写入本轮新增的消息到 AgentArts 记忆库""" if MemoryClient is None or TextMessage is None or not config.memory_space: return try: text_messages = [] for msg in new_messages: if isinstance(msg, HumanMessage) and msg.content: text_messages.append(TextMessage(role="user", content=msg.content, actor_id=_ACTOR_ID, assistant_id=_ASSISTANT_ID)) elif isinstance(msg, AIMessage) and msg.content: text_messages.append(TextMessage(role="assistant", content=msg.content, actor_id=_ACTOR_ID, assistant_id=_ASSISTANT_ID)) if text_messages: MemoryClient().add_messages( space_id=config.memory_space, session_id=session_id, messages=text_messages ) print(f"[Memory] 增量同步 {len(text_messages)} 条新消息到云端") except Exception as e: print(f"[Memory] 云端同步失败(不影响对话): {e}")
步骤三:本地功能验证
在本地直接运行智能体,确认智能体可以正常运行。
- 创建main.py文件,用于本地运行智能体,并进行测试。 测试脚本为交互式对话,可直接输入问题测试agent.py中集成的基础对话、网关、沙箱工具、记忆恢复等能力。
""" 出行规划助手 - 交互式场景演示 """ import sys import uuid from agent import TravelAgent _EXIT_COMMANDS = {"exit", "quit", "退出"} def safe_input(prompt="你: "): """ 兼容 Cloud Shell 终端编码的输入函数。 Cloud Shell 可能以 GBK 编码发送中文字节,而 Python 默认按 UTF-8 解码会报 UnicodeDecodeError。 策略:先尝试 UTF-8,失败后回退到 GBK,再失败则用 replace 模式兜底。 """ print(prompt, end="", flush=True) raw = sys.stdin.buffer.readline() if not raw: return "" try: return raw.decode("utf-8").strip() except UnicodeDecodeError: try: return raw.decode("gbk").strip() except UnicodeDecodeError: return raw.decode("utf-8", errors="replace").strip() def print_guide(): print("\n" + "=" * 60) print(" 杭州周末出行规划助手") print("=" * 60) print(""" 我可以帮你: - 查天气,判断出行是否合适 - 查地点、规划路线 - 计算行程预算 - 记住你的偏好,持续优化方案 建议对话流程: 1. "我周末想去杭州玩,先帮我看看天气" 2. "查一下西湖和灵隐寺的位置" 3. "这两个地方之间怎么走?多远?" 4. "帮我算一下两天行程的总预算" 5. "如果改成三天呢?住宿多一晚" """) print("=" * 60 + "\n") def main(): print("正在初始化智能体助手(集成网关、代码解释器与云端记忆)...") agent = TravelAgent() session_id = str(uuid.uuid4()) print(f"会话 ID: {session_id}") print_guide() while True: try: user_input = safe_input() if not user_input: continue if user_input.lower() in _EXIT_COMMANDS: print("再见!祝旅途愉快~") break print("\n" + "-" * 40) response = agent.run(user_input, session_id=session_id) print(f"\n助手: {response}\n") print("-" * 40 + "\n") except KeyboardInterrupt: print("\n再见!") break if __name__ == "__main__": main()
- 将agent.py、.env、main.py上传至服务器项目目录中。

- 执行python main.py命令对智能体进行本地测试。 由于main.py是交互式对话程序,运行后可直接输入问题,智能体会调用大模型进行回复。有正常的回复即表示本地测试成功。

交互场景与组件调用
问题 1:"我周末想去杭州玩,先帮我看看天气"
- 用户意图:查询目的地天气,判断出行日期是否合适
- 调用组件:网关、记忆库
- 组件作用:
- MCP Gateway:LLM 决策调用 maps_weather 工具,Gateway 转发请求到高德天气服务,返回杭州未来几天的天气数据(温度、天气状况、风向)。
- 记忆库:首次对话,MemoryClient().create_memory_session() 在云端创建记忆会话,为跨会话持久化做准备;get_last_k_messages(k=20) 加载 0 条历史(首轮无历史);对话后增量同步 3 条新消息(用户输入 + AI 工具调用 + AI 最终回复)到云端记忆库。
问题 2:"查一下西湖和灵隐寺的位置"
- 用户意图:获取两个景点的地理坐标和距离
- 调用组件:网关、记忆库
- 组件作用:
- MCP Gateway:LLM 连续调用 3 次工具——maps_geo(西湖坐标)、maps_geo(灵隐寺坐标)、maps_distance(两点驾车距离 4.4 公里)。LLM 自主决策调用顺序和参数,Gateway 透传执行。
- 记忆库:对话前 get_last_k_messages(k=20) 从云端加载 3 条历史消息(Turn 1 的对话),LLM 能记住"之前查了杭州天气"的语境;对话后增量同步 4 条新消息到云端记忆库。
问题 3:"这两个地方之间怎么走?多远?"
- 用户意图:获取多种出行方式的路线规划
- 调用组件:网关、记忆库
- 组件作用:
- MCP Gateway:LLM 一次性调用 4 个路径规划工具——maps_direction_driving(驾车)、maps_direction_walking(步行)、maps_direction_bicycling(骑行)、maps_direction_transit_integrated(公交)。Gateway 并行执行,返回 4 种出行方式的路线数据。安全截断机制检查每个结果长度,超过 8000 字符时会在 JSON 结构边界处截断,避免破坏数据完整性,防止云端记忆写入失败。
- 记忆库:对话前 get_last_k_messages(k=20) 从云端加载 7 条历史消息(Turn 1-2 的对话),LLM 记住前两轮的景点坐标和天气数据,回复中引用"结合周末雨天情况"给出出行建议;对话后增量同步 3 条新消息到云端记忆库。
问题 4:"帮我算一下两天行程的总预算"
- 用户意图:精确计算两日行程的费用明细
- 调用组件:代码解释器、记忆库
- 组件作用:
- 代码沙箱:LLM 不调用 MCP 工具,直接调用 execute_python,在沙箱中执行 Python 代码——定义住宿(经济型 300 元/晚)、餐饮、交通等变量,用代码做加法计算总费用,返回精确结果。LLM 基于沙箱返回的数值生成预算明细表格。
- 记忆库:对话前 get_last_k_messages(k=20) 从云端加载 10 条历史消息(Turn 1-3 的对话),LLM 记住前 3 轮的景点和路线信息,在预算计算中引用之前查询的结果;对话后增量同步 3 条新消息到云端记忆库。
问题 5:"如果改成三天呢?住宿多一晚"
- 用户意图:基于已有两日预算做增量调整
- 调用组件:代码解释器、记忆库
- 组件作用:
- 代码沙箱:LLM 生成新的 Python 代码,复用 Turn 4 的变量定义,增加第三天的费用项(+1 晚住宿 300 元 +1 天餐饮 280 元 +1 天交通 40 元 + 额外景点 80 元),沙箱执行增量计算,返回 +700 元的差额。
- 记忆库:对话前 get_last_k_messages(k=20) 从云端加载 13 条历史消息(Turn 1-4 的对话),LLM 记住 Turn 4 的两天预算方案,能做增量调整而非重新计算;对话后增量同步 3 条新消息到云端记忆库,保存完整 5 轮对话上下文,下次开新会话能加载历史继续规划。
步骤四:SDK封装与本地接口验证
将智能体封装为符合AgentArts平台规范的HTTP服务,并在本地模拟云端接口调用,确认封装正确后再执行后续的部署上云操作。
- SDK封装,AgentArts平台通过标准HTTP协议与智能体通信,您需要用AgentArts SDK将Agent逻辑包一层,将其暴露为符合平台规范的/invocations接口,平台才能正确调度和管理您的智能体。 创建app.py文件,并上传至服务器中。该文件会将本地的Agent逻辑封装为符合AgentArts平台规范的Web服务。
""" AgentArts 平台 HTTP 封装层 将 TravelAgent 暴露为符合平台规范的 /invocations 接口 """ import socket import uuid from typing import Dict, Any # ========================================== # DNS 补丁:容器内无法解析 AgentArts 公网域名 # 必须在 import agent / sdk 之前执行 # 通过 nslookup <域名> 获取对应 IP,替换下面的 XXX.XXX.XXX.XXX # ========================================== _DNS_OVERRIDES = { "memory.cn-southwest-2.huaweicloud-agentarts.com": "XXX.XXX.XXX.XXX", "gateway-gaodemcp-defaultgw-ge3wkzbthf.cn-southwest-2.huaweicloud-agentarts.com": "XXX.XXX.XXX.XXX", "defaultgw-ge3wkzbthf.cn-southwest-2.huaweicloud-agentarts.com": "XXX.XXX.XXX.XXX", } _orig_getaddrinfo = socket.getaddrinfo def _patched_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): if host and host in _DNS_OVERRIDES: return _orig_getaddrinfo(_DNS_OVERRIDES[host], port, family, type, proto, flags) return _orig_getaddrinfo(host, port, family, type, proto, flags) socket.getaddrinfo = _patched_getaddrinfo from agentarts.sdk import AgentArtsRuntimeApp, RequestContext from agent import TravelAgent app = AgentArtsRuntimeApp() try: my_agent = TravelAgent() except Exception as e: print(f"Agent 初始化失败: {e}") my_agent = None def _resolve_session_id(context: RequestContext = None, payload: Dict[str, Any] = None) -> str: raw = None if context: raw = getattr(context, "session_id", None) if not raw and payload: raw = payload.get("session_id") if not raw: raw = str(uuid.uuid4()) return str(raw) @app.entrypoint def invoke(payload: Dict[str, Any], context: RequestContext = None) -> Dict[str, Any]: message = payload.get("message", "") session_id = _resolve_session_id(context, payload) if my_agent is None: return {"response": "Agent 初始化失败", "status": "error", "session_id": session_id} try: response = my_agent.run(message, session_id=session_id) return {"response": response, "status": "success", "session_id": session_id} except Exception as e: return {"response": f"处理失败: {str(e)}", "status": "error", "session_id": session_id} if __name__ == "__main__": app.run(host="0.0.0.0", port=8080) - 接口验证,在正式推送镜像之前,先在本地模拟云端HTTP调用环境,验证Agent的接口封装是否正确、通信是否正常,可以大幅降低因接口问题导致云端部署后才发现错误的调试成本。 执行python app.py启动http server,执行以下命令调用验证Agent的HTTP接口是否已经被正确封装且能正常通信。 执行python app.py回显效果如下。

打开一个新的终端窗口(保持原窗口运行),使用curl命令进行测试。测试完成后,可以使用Ctrl + C停止运行的进程。
curl --location --request POST 'http://localhost:8080/invocations' \ --header 'Content-Type: application/json' \ --data-raw '{"message": "你好,请用一句话做个自我介绍,并查询下杭州的天气。"}'
步骤五:云端部署
将本地智能体打包为Docker镜像并一键部署至AgentArts云端运行时。
- 部署智能体运行时。 按上述步骤完成后,基本代码开发已经完成,接下来准备将本地创建好的智能体部署托管到AgentArts平台。 首先准备依赖文件requirements.txt,内容可参考如下:
# ================================================================================ # AgentArts LangGraph Agent Demo - 依赖清单 # ================================================================================ # # 本项目基于 AgentArts 平台,使用 LangGraph 框架构建 AI Agent # # 安装方式: # pip install -r requirements.txt # # ================================================================================ # 指定国内 PyPI 镜像源 --index-url https://repo.huaweicloud.com/repository/pypi/simple --trusted-host repo.huaweicloud.com # ================================================================================ # LangGraph & LangChain 核心框架 # ================================================================================ langgraph>=0.2.0 langchain>=0.3.0 langchain-core>=0.3.0 langchain-openai>=0.1.0 # ================================================================================ # LLM Provider(ModelArts MaaS OpenAI 兼容接口) # ================================================================================ openai>=1.0.0 # ================================================================================ # HTTP & 网络 # ================================================================================ requests>=2.31.0 urllib3>=2.0.0 # ================================================================================ # 工具与配置 # ================================================================================ python-dotenv>=1.0.0 pydantic>=2.0.0 # ================================================================================ # AgentArts 平台 SDK # ================================================================================ agentarts-sdk>=0.1.0
- 执行如下命令配置智能体。
agentarts configure --entrypoint app:app
执行后按照操作指引进行配置。 配置智能体名称(以小写字母开头,以小写字母或数字结尾,可以包含小写字母、数字和中划线)、服务部署区域(使用cn-southwest-2,仅支持此区域)、requirements.txt依赖文件、SWR Organization镜像组织名称(如果使用自定义的镜像组织名,需要在SWR服务控制台贵阳一region创建)。

- 执行命令部署智能体。
agentarts launch
该命令会自动完成以下步骤:
常见问题
- 执行agentarts launch命令时 出现AK/SK认证报错 执行以下命令配置华为云凭证,获取华为云凭证请参考认证鉴权。
export HUAWEICLOUD_SDK_AK="your-access-key" export HUAWEICLOUD_SDK_SK="your-secret-key"

- 执行agentarts launch命令出现运行时Runtime名称错误
智能体的名称需要以小写字母开头,以小写字母或数字结尾,可以包含小写字母、数字和中划线,长度为2-48个字符。
请重新执行agentarts configure --entrypoint app:app命令进行配置。

- 执行命令,出现SWR.4040010,error_msg:Repository does not exists,ERROR: Organization 'xxx' not found报错。
问题现象及原因
SWR服务镜像组织不存在,出现该报错的原因通常是在执行agentarts configure --entrypoint app:app命令时,手动指定了一个SWR镜像组织名称,但是未在SWR控制台创建同名组织导致。
解决方案
登录SWR服务控制台(贵阳一区域),在“组织管理”中手动创建一个与agentarts configure --entrypoint app:app命令中同名的镜像组织。

- 执行python main.py或者python app.py命令出现Memory记忆库报错。
请检查记忆库ID以及记忆库API Key配置是否正确,建议重新配置该值。

- 执行agentarts invoke与智能体进行会话出现invoke_agent failed、Failed to create sandbox且平台运行时日志出现python: exec format error报错。
问题现象及原因
当前AgentArts服务部署于贵阳一区域,在创建智能体镜像时需要使用ARM64机器打镜像,使用X86机器打镜像会出现此报错。

解决方案
请更换为ARM64机器制作智能体镜像。
- ECS DNS 解析失败
问题现象
在ECS上运行python main.py本地测试时,AgentArts公网域名无法解析,报错NameResolutionError: Failed to resolve。涉及的域名包括(具体域名因region和实例不同而异,请在 AgentArts 控制台查看):
- memory.<region>.huaweicloud-agentarts.com(记忆库服务)
- gateway-<网关名称>-<网关实例ID>.<region>.huaweicloud-agentarts.com(网关)
- defaultgw-<网关实例ID>.<region>.huaweicloud-agentarts.com(沙箱代码解释器)
根因分析
AgentArts域名是公网域名,但ECS默认只配置了华为云内网DNS(100.125.x.x)。内网DNS对这些公网域名返回NXDOMAIN(域名不存在)。此外,ECS使用systemd-resolved管理 DNS,eth0网卡的 +DefaultRoute标志会用 DHCP 分配的内网DNS覆盖全局DNS配置,导致即使修改了/etc/systemd/resolved.conf也无法生效。
解决方案
绕过systemd-resolved,直接写静态/etc/resolv.conf:
# 删除 systemd-resolved 的符号链接 sudo rm /etc/resolv.conf # 写入静态 DNS 配置(单行命令,不用 heredoc) echo -e "nameserver 114.114.114.114\nnameserver 8.8.8.8\nnameserver 100.125.1.250\nsearch openstacklocal" | sudo tee /etc/resolv.conf
DNS 服务器选择说明:
- 114.114.114.114(中国公共 DNS)作为首选——国内访问延迟低,解析速度快
- 8.8.8.8(Google 公共 DNS)作为备选——但国内访问有间歇性超时,不能作为首选
- 100.125.1.250(华为云内网 DNS)——用于解析华为云内部域名
验证方式:
修改后用 nslookup(不带 DNS 服务器参数)逐个测试AgentArts 域名,全部返回 IP 地址即为成功。
更多业务场景
场景配置说明
本智能体采用解耦架构设计:工具动态发现、配置通过环境变量注入、System Prompt 独立定义。移植至其他业务场景时,仅需调整少量配置项,核心代码无需改动。
| 修改类别 | 具体内容 |
|---|---|
| 必须修改 | System Prompt — 定义智能体角色、能力描述与工作原则。当前为出行规划场景定制,移植时需根据新场景重写。 |
| 必须修改 | .env 中的 Gateway 配置 — 若新场景接入不同的 MCP Gateway(如数据库查询工具集),需更新 GATEWAY_ENDPOINT 与 GATEWAY_INBOUND_TOKEN。工具采用动态发现机制,业务代码无需修改。 |
| 建议修改 | main.py 引导文本 — print_guide() 中的场景描述与建议对话流程。属界面展示层,不影响核心逻辑。 |
| 建议修改 | .env 模型配置 — 若新场景对推理能力有更高要求,可调整 MODEL_NAME(如 deepseek-v3)。代码层自动适配。 |
| 可选修改 | 追加本地工具 — execute_python 具备通用性,建议保留。如需扩展其他本地工具(如发邮件、文件操作),定义后追加 StructuredTool 即可。 |


