[实践]-本地大模型Agent使用自定义MCP服务实现与SAP系统集成(二)

实践-本地大模型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_agenthttpxmcp 等库的 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_inforead_table_data 工具。

该过程是完全自动化的,无需用户手动干预。

  1. 解释器路径是头号坑点 :stdio 模式下,command 如果写 python3 可能指向系统 Python 而非虚拟环境。务必使用绝对路径指向 .venv/bin/python3
  2. Docstring 即契约:不要写"获取数据"这种模糊描述,应写"读取SAP指定表名的业务数据,参数为表名(如T001),返回JSON格式结果"。
  3. SAP OData 语法细节 :字符串主键必须用单引号 包裹;$format=json 比 Accept Header 更可靠;sap-client 建议走 URL 参数。
  4. 日志分离:Agent 侧和 Server 侧的日志一定要分开。Server 作为子进程,其 stdout/stderr 会被 MCP 协议占用,业务日志只能写入文件或通过专门的日志通道传输。
  5. 思维链的价值 :开启 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来重构一遍!

相关推荐
2601_965742222 小时前
全媒体运营与短视频代运营,两者有什么区别?
大数据·数据结构·人工智能·算法·ai·媒体
樊小肆2 小时前
离谱,每轮请求 25% 的 token,竟在重发模型想完就扔的内心独白
前端·人工智能·agent
SamChan903 小时前
PDF翻译后的格式完整性校验:用Python自动比对译文与原文档的表格与段落结构
开发语言·python·ai·pdf·机器翻译
七牛开发者3 小时前
告别反复调参,一个 Skill 让 AI 掌握论文图的视觉语法:以 DeepSeek V4.1 Flash 论文图为例
github·agent·deepseek
阿里云大数据AI技术3 小时前
DataWorks Data Agent 实战课堂(八):数据质量巡检服务
人工智能·agent
一只小bit3 小时前
LlamaIndex框架:简单RAG框架的全过程实现手册
llm·milvus·rag·llamaindex·deepseek
GHL2842710903 小时前
豆包以图生图学习
学习·ai
酒旅Agent开发实战4 小时前
酒店供应链MCP实践分享
人工智能·大模型·酒店预订·ai agent·mcp
奇牙coding1234 小时前
GPT-5.5 升级 GPT-5.6 接入指南:流式调用配置与常见问题
java·网络·gpt·ai