实践-本地大模型Agent使用自定义MCP服务实现与SAP系统集成(二)
本实践基于 Qwen-Agent + FastMCP 技术栈,完整演示如何构建一个自定义 MCP Server 对接 SAP OData 服务,并让 Agent 通过 stdio 模式自动拉起服务、发现工具并完成业务查询。
🏗️ 整体架构设计
在本实践中,我们采用"Agent 作为主控,MCP Server 作为能力插件"的架构:
text
┌─────────────────┐ stdio / MCP 协议 ┌──────────────────────┐
│ Qwen-Agent │ ◄──────────────────────► │ FastMCP Server │
│ (Assistant) │ 工具发现 → 调用 → 返回 │ (SAPMCPServer.py) │
└────────┬────────┘ └──────────┬───────────┘
│ │
▼ ▼
OpenAI 兼容 LLM SAP OData API
(Qwen3.6-35B-A3B) (read_table / get_info)
💡 关键设计要点
| 要点 | 说明 |
|---|---|
| 工具注册 | 使用 @mcp.tool() 装饰器,函数的 docstring 即为工具描述。Agent 完全依赖这段描述来决定何时调用该工具,务必写得清晰准确。 |
| 工具命名 | Qwen-Agent 注册后,工具名会自动拼接为 服务器名-工具名,例如 SAPMCPServer-read_table_data,避免多服务冲突。 |
| stdio 拉起 | Agent 会将 MCP Server 作为子进程 自动启动。配置中的 command 必须指向安装了 fastmcp 依赖的 Python 解释器路径。 |
🛠️ 第一步:环境准备
推荐使用 uv 进行项目初始化,确保依赖隔离:
bash
# 初始化项目并创建虚拟环境
uv init AgentCallMCP && cd AgentCallMCP
uv venv
# 安装核心依赖
uv add "mcp[cli]" httpx openai fastmcp qwen-agent
⚠️ 日志治理提示
在 Notebook 或生产环境中,
qwen_agent、httpx、mcp等库的 DEBUG 日志量极大。建议在 Agent 侧将第三方库日志级别设为ERROR,仅保留 WARNING 以上输出;MCP Server 侧的详细日志则独立写入mcp_server.log文件,避免控制台刷屏。
🔌 第二步:封装 SAP OData 服务
在定义 MCP 工具前,先封装好底层的 SAP API 调用逻辑。这里使用 requests.Session 维持会话,并通过 URL 参数传递 sap-client(比 Header 更可靠)。
python
# SAP_Service.py
import os, requests
from urllib.parse import quote
from requests.auth import HTTPBasicAuth
session = requests.Session()
BASE_URL = "http://192.168.31.21:8080/sap/opu/odata/sap/YSAP_SERVICE_SRV/"
session.auth = HTTPBasicAuth(
os.getenv("SAP_USERNAME", "SAP_XXXX"),
os.getenv("SAP_PASSWORD", "1QAZ2WSX")
)
session.headers.update({"Accept": "application/json"})
def get_table_data(table_name):
"""按主键表名读取表数据"""
key = quote(str(table_name), safe="")
url = f"{BASE_URL}DB_DATASet('{key}')?$format=json"
resp = session.get(url, timeout=30)
resp.raise_for_status()
return resp.json()["d"]
def get_table_info(table_name):
"""按主键表名读取表结构信息"""
key = quote(str(table_name), safe="")
url = f"{BASE_URL}DB_INFOSet('{key}')?$format=json"
resp = session.get(url, timeout=30)
resp.raise_for_status()
return resp.json()["d"]
SAP OData 实现如下:
🚀 第三步:实现 FastMCP Server
使用 FastMCP 将上述函数注册为标准化工具。docstring 是 Agent 理解工具用途的唯一依据,请保持简洁明确。
python
# SAPMCPServer.py
import logging
from fastmcp import FastMCP
from SAP_Service import get_table_data, get_table_info
logging.basicConfig(
level=logging.DEBUG,
handlers=[logging.FileHandler('mcp_server.log', encoding='utf-8')]
)
logger = logging.getLogger('SAPMCP-Server')
mcp = FastMCP("SAPMCPService")
@mcp.tool()
def read_table_data(table_name: str) -> str:
"""读取指定SAP表的业务数据,传入表名如 T001"""
logger.info(f"调用工具 read_table_data: {table_name}")
result = get_table_data(table_name)
return result if result else f"读取表 {table_name} 数据失败"
@mcp.tool()
def read_table_info(table_name: str) -> str:
"""读取指定SAP表的结构定义信息,用于了解字段含义"""
logger.info(f"调用工具 read_table_info: {table_name}")
result = get_table_info(table_name)
return result if result else f"读取表 {table_name} 信息失败"
if __name__ == "__main__":
logger.info("🚀 SAPMCP Server 启动中...")
mcp.run()
🤖 第四步:Agent 接入与对话
Qwen-Agent 通过 function_list 中的 mcpServers 配置加载工具。推荐使用 stdio 模式,Agent 会自动管理 Server 子进程的生命周期。
MCP 连接方式对比
| 方式 | 适用场景 | 配置关键字 |
|---|---|---|
| ① stdio(推荐) | 本地开发,Agent 自动拉起子进程 | command + args |
| ② stdio URL | Server 已单独启动,通过管道连接 | url (如 http://127.0.0.1:8001/stdio) |
| ③ streamable-http | 远程/网络服务部署 | type + url |
Agent 代码实现
python
from qwen_agent.agents import Assistant
llm_cfg = {
'model': 'Qwen3.6-35B-A3B-4bit',
'model_server': 'http://localhost:8000/v1',
'api_key': 'sk-XXXX',
'generate_cfg': {
'use_raw_api': True,
'extra_body': {'chat_template_kwargs': {'enable_thinking': True}}
},
}
# ⚠️ command 必须指向装有 fastmcp 的解释器
tools = [{
'mcpServers': {
'SAPMCPServer': {
'command': '/path/to/.venv/bin/python3',
'args': ['/path/to/SAPMCPServer.py'],
}
}
}]
bot = Assistant(llm=llm_cfg, function_list=tools)
# 发起业务查询
prompt = '我想知道SAP会计科目表的表结构,和前10行数据请展示。'
# 多任务示例:
# prompt = ('请依次调用工具完成:'
# '1) 读取公司代码表前 5 行数据,表名不记得了,需要你自己查询;'
# '2) 你可以通过SAP表结构查询工具了解SAP底层表设计,然后读取相关表数据出一份现金流量表;')
messages = [{'role': 'user', 'content': prompt}]
for round_msgs in bot.run(messages=messages):
# 流式处理输出,包含工具调用🔧、工具返回📤、助手回复🤖
for m in round_msgs:
if m.get('function_call'):
print(f"🔧 调用: {m['function_call']['name']}")
if m.get('role') == 'tool':
print(f"📤 返回: {str(m['content'])[:200]}...")
if m.get('role') == 'assistant' and m.get('content'):
print(f"🤖 {m['content']}", end='')
调用结果返回如下(截取部分):
主要是为了解LLM的推理及调用工具的过程。
────── 第 1 轮 ──────
🔧 工具调用: SAPMCPServer-read_table_info
table_name = T030
────── 第 2 轮 ──────
🔧 工具调用: SAPMCPServer-read_table_info
table_name = T030
🔧 工具调用: SAPMCPServer-read_table_data
table_name = T030
────── 第 3 轮 ──────
🔧 工具调用: SAPMCPServer-read_table_info
table_name = T030
🔧 工具调用: SAPMCPServer-read_table_data
table_name = T030
2026-09-08 09:05:27,426 - base.py - 780 - INFO - ALL tokens: 2964, Available tokens: 58000
...
| `KONTS` | G/L Account Number | 总账科目号 | `CHAR` (10) | 主总账科目编号 |
| `KONTH` | G/L Account Number | 总账科目号(替换) | `CHAR` (10) | 总账科目号(通常用于历史或映射) |
....................................................................................
> 从第三轮开始已经成功读取到SAP表数据,后面的的轮次基本都是对数据的处理,因为返回的是全表数据,共经历417轮,最后返回结果。
> 吧啦吧啦输出很多轮的处理过程
>
💡 **业务提示**:
- `T030` 属于 SAP 的**主数据配置表**,通常用于系统初始化时定义不同科目表(如 `ACIA` 国际准则、`BANK` 银行专用等)的全局参数。
- 实际业务中,具体的**总账科目明细行**会存储在另一张核心表 `SKA1` 中,`T030` 主要维护科目表的层级规则与映射关系。
────── 第 420 轮 ──────
....................................................................................
📋 最终回答
════════════════════════════════════════════════════════════
以下是为您获取的 **SAP 会计科目表(表名:`T030`)** 的表结构说明及前 10 行数据展示:
### 📊 表结构说明 (T030)
该表用于存储 SAP 系统中不同公司代码下所使用的**会计科目表(Chart of Accounts)**的全局配置信息。
| 字段名 | 字段描述 (英文) | 字段描述 (中文) | 类型/长度 | 说明 |
|:---|:---|:---|:---|:---|
| `MANDT` | Client | 客户端 | `CLNT` (3) | 系统客户端标识(主键) |
| `KTOPL` | Chart of Accounts | 会计科目表代码 | `CHAR` (4) | 公司所使用的科目表代号(主键) |
| `KTOSL` | Transaction Key | 事务键 | `CHAR` (3) | 标识科目表类型(如 `BIL` 财务科目表, `COA` 控制科目表等) |
| `BWMOD` | Valuation Grouping Code | 估值组合代码 | `CHAR` (4) | 资产估值分组代码 |
| `KOMOK` | Account Modification | 科目修改标记 | `CHAR` (3) | `X` 或 `Y` 或 `Z` 表示不同管理规则 |
| `BKLAS` | Valuation Class | 评估类别 | `CHAR` (4) | 物料评估所在的总账科目类别 |
| `KONTS` | G/L Account Number | 总账科目号 | `CHAR` (10) | 主总账科目编号 |
| `KONTH` | G/L Account Number | 总账科目号(替换) | `CHAR` (10) | 总账科目号(通常用于历史或映射) |
---
### 📋 前 10 行数据展示
*(注:空值区域已用 `-` 补全以便对齐查看)*
| 行号 | 客户端 (MANDT) | 科目表 (KTOPL) | 事务键 (KTOSL) | 估值代码 (BWMOD) | 科目修改 (KOMOK) | 评估类别 (BKLAS) | 总账科目 (KONTS) | 总账科目(KONTH) |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 1 | 800 | - | - | - | - | - | - | - |
| 2 | 800 | ACIA | BIL | - | X | - | 0000900000 | 0000900000 |
| 3 | 800 | ACIA | BIL | - | Y | - | 0080900000 | 0080900000 |
| 4 | 800 | ACIA | BIL | - | Z | - | 0090900000 | 0090900000 |
| 5 | 800 | BANK | BIL | - | X | - | 0090000000 | 0090000000 |
| 6 | 800 | BANK | UMS | - | - | - | 0020106000 | 0020106000 |
| 7 | 800 | CA20 | BIL | - | X | - | 0000900000 | 0000900000 |
| 8 | 800 | CAAU | AKO | 0001 | - | 3000 | 0000520030 | 0000700780 |
| 9 | 800 | CAAU | AKO | 0001 | - | 3030 | 0000520030 | 0000700780 |
| 10| 800 | CAAU | AKO | 0001 | - | 3040 | 0000520030 | 0000700780 |
📝 实践总结
本实践基于 Qwen-Agent + FastMCP 技术栈,完整演示如何构建一个自定义 MCP Server 对接 SAP OData 服务,并让 Agent 通过 stdio 模式自动拉起服务、发现工具并完成业务查询。
在提示词中,只需要告知Agent意图,让LLM根据自己的知识去寻找对应的方法(其实可以给LLM跟多你知道的专业知识,可以让他少走弯路和错路),去发现MCP中注册的工具,然后自主调用这些工具。
例如,用户可以输入"我想知道SAP会计科目表的表结构,和前10行数据请展示",Agent 会自动找表,然后调用
read_table_info和read_table_data工具。该过程是完全自动化的,无需用户手动干预。
- 解释器路径是头号坑点 :stdio 模式下,
command如果写python3可能指向系统 Python 而非虚拟环境。务必使用绝对路径指向.venv/bin/python3。 - Docstring 即契约:不要写"获取数据"这种模糊描述,应写"读取SAP指定表名的业务数据,参数为表名(如T001),返回JSON格式结果"。
- SAP OData 语法细节 :字符串主键必须用单引号 包裹;
$format=json比 Accept Header 更可靠;sap-client建议走 URL 参数。 - 日志分离:Agent 侧和 Server 侧的日志一定要分开。Server 作为子进程,其 stdout/stderr 会被 MCP 协议占用,业务日志只能写入文件或通过专门的日志通道传输。
- 思维链的价值 :开启
enable_thinking=True后,Qwen3 会在调用工具前进行推理规划,显著提升复杂多步任务(如"先查表结构再查数据")的成功率。
通过 MCP 协议,我们将 SAP 这样的重型企业系统变成了 Agent 可即插即用的标准工具。系统中大量的业务数据和现行业务逻辑,都可以通过 Agent 来调用,企业甚至可以通过SAP系统积累的数据和业务逻辑来微调本地部署的模型,这将会让Agent更加聪明和实用,可能还有些未知的奇妙事情发生。
后续方向
由于本次实践都是在本地环境下进行的,主要是为了体验Agent的自我感知->认知->决策->执行->再感知的循环过程。这两天部署SAP系统,搭建Odata开发环境花了很多时间,Odata API中只做了简单的GET请求,后续可以增加POST,PUT,DELETE请求等,增加过滤、排序、分页、聚合等功能,以满足更多业务需求;Agent实现没带Langchain,后续可以考虑添加Langchain来增强Agent的能力,如增加记忆、上下文理解等。
在AI+时代,一切工程都可值得用AI来重构一遍!