本地大模型 + 自建 MCP Server + SAP OData:让 LLM 代理 SAP 业务操作
本文记录从零搭建「本地 LLM → MCP Server → SAP OData」完整链路的全过程,附带解决方案与完整代码;核心思路:把 SAP OData 的查/改/增/删能力封装成 MCP Tool,LLM 通过
function_call自然语言调用。
1. 整体架构
本地 LLM → MCP → SAP OData 分层架构
关键链路用品牌色标出:stdio 通信(蓝色)、HTTP/SAP OData(橙色)、SAP OData 写路径自动处理 CSRF + ETag(绿色)

2. 三层代码实现
第 1 层:SAPODataClient(HTTP 认证 + CSRF + ETag)
SAP OData v2 的写操作有两道安全门:
| 机制 | 触发条件 | 失败状态码 |
|---|---|---|
| X-CSRF-Token | POST/PUT/PATCH/DELETE | 403 |
| If-Match: ETag | PUT/PATCH/DELETE 单实体 | 428 Precondition Required |
python
class SAPODataClient:
def call(self, method, service_path, entity_set=None,
key=None, params=None, payload=None, etag=None):
"""通用 OData 调用,自动处理 CSRF + ETag + 403 重试"""
# ... URL 拼接 ...
# ① 写操作 → 自动拿 CSRF Token
needs_write = method in ('POST', 'PUT', 'PATCH', 'MERGE', 'DELETE')
if needs_write and not self._csrf_token:
self._fetch_csrf_token(service_path) # 从目标服务的 $metadata 取
# ② PATCH/PUT/DELETE 单实体 → 自动拿 ETag
if method in ('PUT', 'PATCH', 'MERGE', 'DELETE') and key:
if not etag:
probe = self.session.get(url, params={'sap-client':'600','$format':'json'})
if probe.status_code == 200:
etag = probe.json()['d']['__metadata']['etag']
elif probe.status_code == 400 and method == 'DELETE':
etag = '*' # DELETE 不存在实体时用通配符绕过 Gateway 检查
if etag:
headers['If-Match'] = etag
# ③ 403 自动刷新 CSRF 重试一次
resp = self.session.request(method, url, ...)
if resp.status_code == 403 and needs_write:
self._fetch_csrf_token(service_path)
headers['X-CSRF-Token'] = self._csrf_token
resp = self.session.request(method, url, ...)
第 2 层:业务封装(发现 + 通用调用)
LLM 不能瞎调 SAP,它必须先"看菜单",再"看菜谱",最后"做菜"。两步发现 + 一步执行:
python
def get_entity_sets(service_url: str) -> list:
"""解析 $metadata XML → EntitySet + 字段 + 主键 + 导航属性"""
resp = session.get(
service_url + '/$metadata',
params={'sap-client': '600'},
headers={'Accept': 'application/xml'}, # JSON 客户端拿 XML 必须显式指定
)
root = ET.fromstring(resp.text)
# 遍历 EntityType 收集 keys/properties/navigation_properties
# 遍历 EntitySet 映射到 EntityType
return [{'name': ..., 'keys': [...], 'properties': [...]}, ...]
def call_odata(service_url, entity_set, method='GET',
options=None, key=None, payload=None, etag=None):
"""通用调用:统一封装返回 + 清洗 SAP 内部字段"""
result = client.call(method_up, service_path, entity_set=entity_set,
key=key, params=options, payload=payload, etag=etag)
# SAP 业务错误(400)不抛异常,透传给 LLM 让它自己读
if isinstance(result, dict) and result.get('__sap_error__'):
return result
if method == 'GET':
return {"count": N, "results": [清洗后的记录]} # 剥掉 __metadata
if method == 'POST':
return {"status": "created", "created": {...}}
if method in ('PUT', 'PATCH'):
return {"status": "updated"}
if method == 'DELETE':
return {"status": "deleted"}
第 3 层:FastMCP Tool 注册
python
# ⚠️ 关键:导入用别名!避免同名遮蔽导致无限递归
from SAP_Service import (
fetch_all_services,
get_entity_sets as _get_entity_sets,
call_odata as _call_odata,
)
@mcp.tool()
def call_odata(service_url: str, entity_set: str, method: str = "GET",
options: dict = None, key: str = None,
payload: dict = None, etag: str = None) -> dict:
"""通用 SAP OData 调用,支持 GET/POST/PATCH/DELETE..."""
try:
return _call_odata(service_url, entity_set, method=method,
options=options, key=key, payload=payload, etag=etag)
except Exception as e:
logger.error(f"OData 调用失败: {e}")
return {"error": str(e)}
3. LLM 自然语言调用流程
LLM Agent 自然语言操作 SAP 的调用链路

用户问:"帮我查一下客户组 0001 的描述,然后改成'测试组'"。LLM 自动触发三轮工具调用:
┌─ Step 1: get_all_services() ─────────────────────────────┐
│ LLM: "我需要找到客户组相关的服务" │
│ → 返回 13 个 ZAPI_CU 服务, 选中 ZAPI_CUSTOMERGROUP_SRV │
└─────────────────────────────────────────────────────────┘
↓
┌─ Step 2: get_entity_sets(service_url) ──────────────────┐
│ LLM: "这个服务下有哪些实体集?字段是什么?" │
│ → A_CustomerGroup(keys=[CustomerGroup]) │
│ A_CustomerGroupText(keys=[Language,CustomerGroup]) │
│ properties: [CustomerGroupName, ...] │
└─────────────────────────────────────────────────────────┘
↓
┌─ Step 3: call_odata(..., method='PATCH', payload={...}) ─┐
│ LLM: "描述在 Text 表里,用复合键定位,然后 PATCH" │
│ ① GET $metadata → X-CSRF-Token │
│ ② probe GET 实体 → ETag: W/"..." │
│ ③ PATCH + CSRF + If-Match → 204 No Content │
│ → {"status": "updated"} │
└─────────────────────────────────────────────────────────┘
4. 测试验证结果
| 操作 | 管道状态 | 典型响应 |
|---|---|---|
| GET 列表 | ✅ | {"count":2,"results":[...]} 支持 filter/filter/filter/select/top/top/top/orderby/$expand |
| GET 单实体 | ✅ | 支持复合键 (Language='EN',Industry='0001') |
| PATCH 更新 | ✅ | 自动 ETag + If-Match,返回 {"status":"updated"} |
| POST 创建 | ✅ | CSRF 自动管理,业务校验错误完整透传 |
| DELETE 删除 | ✅ | DELETE 不存在实体时用 If-Match:* 绕过 Gateway |
5. 结语
这条链路打通后,本地 LLM 就能像业务用户一样操作 SAP 了:读数据、改状态、建单据------全部自然语言。本次实践只关注技术实现可行性,具体应用还需要对各个服务进行封装测试并确保准确无误及安全可靠,比如:
- 权限隔离:MCP Server 的 SAP 账号应只给业务必需的最小权限,不要拿超级用户
- 写操作确认:对 POST/PATCH/DELETE 加二次确认机制(比如 LLM 先给用户预览 payload)
- 审计日志:所有写操作落盘,便于追溯
MCP 协议的价值不只是"让 LLM 调 API",而是提供了一套结构化的能力暴露机制------SAP 的查/改/增/删被拆解成三个 MCP Tool,LLM 可以自主发现、自主组合、自主修正,真正实现 Agent 代理业务操作。
在AI+时代,一切工程都值得用AI来重构一遍!