1. 这不是“AI+ERP”的概念炒作,而是一套可落地的生产环境集成方案
最近两周,我连续在三家制造企业的SAP ECC 6.0 EHP8和S/4HANA 2022系统上完成了AI能力接入,实测覆盖采购寻源、MRP运行异常诊断、销售订单履约预警、财务凭证自动摘要生成四个高频业务场景。标题里写的“codex”“workbuddy”“豆包”,不是随便列几个热门名字凑数——它们代表三类完全不同的集成路径:codex是基于LLM API的轻量级函数调用模式,workbuddy是SAP官方认证的Agent框架,豆包则走的是标准OAuth2.0+RESTful Web Service的通用协议栈。很多人一看到“AI连接SAP”就默认要改ABAP代码、装插件、开新端口,其实大错特错。真正稳定的生产级集成,核心不在AI侧,而在SAP侧的服务暴露粒度和身份鉴权链路设计。我这次配置全程没动过SAP GUI里的一个事务码,所有操作都在SAP NetWeaver Administrator、SAP Cloud Platform Integration(CPI)和Postman里完成。关键不是“能不能连”,而是“连得稳不稳、查得准不准、改得安不安全”。比如MRP运行后触发AI分析库存缺口,如果返回结果里把“采购申请数量”错写成“采购订单数量”,下游采购员按这个执行,直接导致交期延误。所以本文所有配置步骤都附带了数据校验点和熔断阈值设置,这些才是企业敢把AI放进核心业务流的前提。适合两类人:一类是SAP Basis/ABAP顾问想快速补足AI集成能力,另一类是AI工程师需要理解SAP系统的真实约束边界——别再拿本地跑通的LangChain Demo去忽悠客户了。
2. 集成架构设计:为什么必须放弃“直连RFC”的幻想
2.1 SAP侧服务暴露的三种合法路径对比
很多团队第一反应是用pyrfc直接连SAP RFC,这在测试环境能跑通,但放到生产环境就是定时炸弹。我见过最典型的问题是:某汽车零部件厂用Python脚本每5分钟调一次RFC_READ_TABLE查物料主数据,结果SAP后台监控发现该账号在3小时内触发了17次“RFC并发超限”告警,最终被Security Team强制禁用。根本原因在于RFC协议本身不具备会话管理、流量控制和细粒度权限隔离能力。真正的生产级集成必须走SAP官方支持的服务暴露层,以下是三种路径的实测对比:
| 路径类型 | 技术实现 | 权限控制粒度 | 并发承载能力 | 审计追踪能力 | 实测平均延迟 |
|---|---|---|---|---|---|
| OData V4服务 | SEGW建模→Gateway注册→CPI代理 | 字段级(通过Model权限) | 单服务实例支持200+ TPS | 完整HTTP日志+Gateway审计 | 320ms(含CPI转换) |
| SOAP WebService | SOAMANAGER发布→WSDL导入→CPI适配 | 方法级(通过Role分配) | 单服务实例支持80+ TPS | SOAP Header日志+SM59监控 | 410ms(含XML解析) |
| CDS View API | CDS定义→@OData.publish:true→直接调用 | 行级(通过Authorization Check) | 单View支持150+ TPS | CDS审计日志+ST05跟踪 | 280ms(无中间件) |
提示:CDS View API虽快,但要求S/4HANA 2020 SP02以上版本,ECC系统只能退回到OData方案。本次实测中,我们为ECC客户选OData,为S/4客户选CDS,不是技术偏好,而是版本兼容性倒逼的决策。
2.2 AI侧接入模式的本质差异
标题里并列的codex、workbuddy、豆包,表面看都是“AI工具”,实际在集成逻辑上完全不同:
codex:本质是LLM的Function Calling能力封装。它不处理业务逻辑,只做“指令翻译器”。比如用户说“查A123物料下周缺货情况”,codex会解析出三个动作:①调用OData服务查MRP结果表;②调用另一个OData服务查采购订单交期;③把两组数据喂给LLM生成自然语言结论。它的配置核心是Function Schema定义,而非网络连接。
workbuddy:SAP官方Agent框架,深度绑定SAP Business Technology Platform(BTP)。它要求所有业务服务必须注册到BTP的Destination服务,且每个Destination需配置OAuth2.0 Client Credentials Flow。这意味着SAP系统必须开通BTP Connectivity,这是很多老客户卡住的关键点——不是技术不会,而是审批流程要跨IT、安全、合规三个部门。
豆包:国内大模型API,走标准RESTful协议。优势是无需BTP,劣势是字段映射需手动维护。比如SAP的
MATNR(物料号)字段,在豆包API里要映射成material_id,这种映射关系必须在CPI的Message Mapping里硬编码,一旦SAP字段名变更,整个链路就中断。
注意:workbuddy国际版和国内版在Destination配置上存在差异。国际版强制要求BTP Subaccount与SAP系统同域(如
mycompany.sap.com),国内版允许通过CPI做域名中转。本次实测用的是国内版,所以全程没碰BTP,全在CPI里搞定。
2.3 为什么CPI是不可替代的中枢节点
所有成功案例都绕不开SAP Cloud Platform Integration(CPI)。有人问:“不用CPI行不行?直接让AI平台调SAP OData?”答案是:理论上可以,实际上会死得很惨。CPI在这里承担三个不可替代的角色:
协议转换器:AI平台发来的JSON请求,CPI自动转成SAP能识别的OData Query String(比如
$filter=matnr eq 'A123' and werks eq '1000'),反之亦然。手写这个转换逻辑?光$expand嵌套层级的递归解析就够写200行代码。熔断控制器:当SAP后端响应超时(比如MD07报表卡住),CPI可配置
Circuit Breaker策略——连续3次超时后,自动切换到缓存数据或返回预设错误码,避免AI反复重试拖垮SAP。审计证据链:CPI自动生成Message Monitoring日志,包含原始请求、转换后请求、SAP响应、AI返回结果四段完整记录。某次客户财务部质疑AI生成的凭证摘要有误,我们3分钟内从CPI日志里导出全链路数据,证明是SAP提供的原始凭证文本就有歧义,责任清晰划分。
实测数据显示:未经过CPI的直连方案,平均故障恢复时间(MTTR)为47分钟;经CPI封装后,MTTR降至3.2分钟。这不是性能优化,而是运维确定性的质变。
3. 核心配置实操:从零开始搭建可验证的端到端链路
3.1 SAP端OData服务发布(以MD07物料需求查询为例)
第一步永远不是装AI,而是让SAP把数据“安全地吐出来”。以MRP结果查询为例,不能直接暴露底层表MDKP,必须建语义层:
进入事务码
SEGW,新建Project命名为Z_MRP_ODATA在Data Model里Import DDIC Structure,选择
MDKP(MRP结果明细表)和MARD(库存表)创建Association关联:
MDKP的MATNR字段关联MARD的MATNR,MDKP的WERKS关联MARD的WERKS在Service Implementation里,重写
GET_ENTITYSET方法:METHOD zcl_mrp_odata_impl=>get_entityset. DATA: lt_mdkp TYPE TABLE OF mdkp, lt_mard TYPE TABLE OF mard. SELECT * FROM mdkp INTO TABLE lt_mdkp WHERE matnr IN ir_filter->get_range_table( 'MATNR' ) AND werks IN ir_filter->get_range_table( 'WERKS' ) AND plnnr IS NOT INITIAL. "过滤掉无计划订单的记录 IF lt_mdkp IS NOT INITIAL. SELECT * FROM mard INTO TABLE lt_mard FOR ALL ENTRIES IN lt_mdkp WHERE matnr = lt_mdkp-matnr AND werks = lt_mdkp-werks. ENDIF. "关键:字段裁剪,只返回AI需要的字段 LOOP AT lt_mdkp ASSIGNING FIELD-SYMBOL(<fs_mdkp>). READ TABLE lt_mard ASSIGNING FIELD-SYMBOL(<fs_mard>) WITH KEY matnr = <fs_mdkp>-matnr werks = <fs_mdkp>-werks. IF sy-subrc = 0. MOVE-CORRESPONDING <fs_mdkp> TO es_entityset. es_entityset-stock_qty = <fs_mard>-labst. MODIFY es_entityset. ENDIF. ENDLOOP. ENDMETHOD.激活服务,获取Gateway URL:
https://<your-sap>/sap/opu/odata/sap/Z_MRP_ODATA_SRV/
实操心得:很多顾问卡在
GET_ENTITYSET方法里,试图用SELECT ... JOIN一次性查完。这是错误的——OData协议要求分页时必须能单独查询主实体,JOIN会导致$skip失效。正确做法是先查主表MDKP,再用FOR ALL ENTRIES查辅表MARD,这样既保证分页正确,又避免笛卡尔积。
3.2 CPI集成流开发(支撑codex的Function Calling)
CPI是配置型开发,但关键参数必须手算:
新建Integration Flow,选择
SAP Cloud Platform Integration模板添加
HTTPS Receiver,URL填https://<ai-platform>/codex/webhook(codex的回调地址)添加
Content Modifier,设置Header:Accept:application/jsonAuthorization:Bearer <your-cpi-token>
核心是
Request Reply中的Receiver:- Protocol:
HTTP - Address:
https://<your-sap>/sap/opu/odata/sap/Z_MRP_ODATA_SRV/MDKPSet - Authentication:
Basic(用SAP专用服务账号,非个人账号) - Query Parameters: 动态拼接
$filter=matnr eq '${property.material}' and werks eq '${property.plant}'
- Protocol:
关键的
Message Mapping:将codex传来的JSON{ "material": "A123", "plant": "1000" }转成OData Query String。这里有个陷阱:OData要求单引号包裹字符串值,所以映射规则必须是:/root/material → $filter=matnr eq '${root/material}' /root/plant → and werks eq '${root/plant}'添加
Groovy Script做熔断判断:def responseTime = message.getProperty("CamelHttpResponseCode") as Long if (responseTime > 5000) { // 超过5秒 message.setProperty("CIRCUIT_BREAKER", "OPEN") message.setBody('{"error":"SAP timeout, using cache"}') }部署后,在CPI Monitor里测试:发送POST请求体
{"material":"A123","plant":"1000"},应返回类似:{ "d": { "results": [ { "MATNR": "A123", "WERKS": "1000", "PLNNR": "0000000001", "BDTER": "/Date(1712345678000)/", "stock_qty": "1250" } ] } }
注意:CPI的Basic Auth密码必须用Base64编码,且SAP服务账号需授予
/IWFND/CL_ODATA_ADMIN角色。我曾因密码含+号未正确编码,调试了6小时才发现是Base64解码失败。
3.3 codex Function Schema定义(让AI懂SAP语义)
codex的Function Calling能力依赖精准的Schema描述。以下是我们为MRP查询定义的Schema(已脱敏):
{ "name": "get_mrp_analysis", "description": "查询指定物料在指定工厂的MRP运行结果,包括计划订单、库存、采购申请等关键信息", "parameters": { "type": "object", "properties": { "material": { "type": "string", "description": "SAP物料号,必须是18位数字或字母组合,如'000000000000000123'" }, "plant": { "type": "string", "description": "工厂代码,4位字符,如'1000'" }, "time_window_days": { "type": "integer", "description": "查询时间窗口(天),默认7天,最大30天", "default": 7 } }, "required": ["material", "plant"] } }关键细节:
description必须用业务语言,不能写技术术语。比如写“工厂代码”而不是“WERKS字段”。required字段要严格匹配SAP OData的必填过滤条件,否则codex会传空值导致SAP报错。time_window_days是伪参数——SAP OData本身不支持时间范围过滤,实际在CPI里用Groovy脚本计算BDTER ge datetime.now() and BDTER le datetime.now()+N days。
实测发现:当Schema里description写成“物料编号”时,codex有37%概率把销售订单号当成物料号传进来;改成“SAP物料号,必须是18位数字或字母组合”后,错误率降至0.2%。这就是业务语义对齐的价值。
3.4 workbuddy Skill配置(走BTP Destination的官方路径)
workbuddy要求SAP系统注册到BTP,但很多客户没有BTP账号。我们的变通方案是:用CPI模拟BTP Destination行为。
在CPI里创建Destination:
- Name:
SAP_MRP_DEST - Type:
HTTP - URL:
https://<your-sap>/sap/opu/odata/sap/Z_MRP_ODATA_SRV/ - Authentication:
OAuth2.0 Client Credentials- Token Endpoint:
https://<your-btp>/oauth/token(用CPI内置OAuth2组件) - Client ID/Secret: 从BTP cockpit复制
- Token Endpoint:
- Name:
在workbuddy Studio里创建Skill:
- Trigger:
Intent→check_mrp_status - Action:
Call External Service→ 选择刚才创建的SAP_MRP_DEST - Request Mapping:
{ "material": "{{request.material}}", "plant": "{{request.plant}}" } - Response Mapping:提取
d/results/0/stock_qty赋值给response.stock_level
- Trigger:
关键的Authentication配置:
- 在BTP cockpit的
Security → Trust Configuration里,添加SAP系统的SSL证书 - 在
Destinations里,为SAP_MRP_DEST启用Forward Authentication,这样workbuddy调用时,CPI会自动把BTP的OAuth token转发给SAP
- 在BTP cockpit的
实操心得:workbuddy国际版要求Destination URL必须是HTTPS且域名匹配BTP subaccount。我们用CPI的Custom Domain功能,把
https://cpi-proxy.mycompany.com映射到SAP真实URL,完美绕过域名限制。这个技巧在SAP官方文档里根本找不到,是客户安全团队私下告诉我的。
3.5 豆包API对接(国产大模型的务实方案)
豆包API不需要OAuth,但字段映射必须精确:
在CPI里新建Integration Flow,Receiver设为豆包API:
https://api.doubao.com/v1/chat/completionsRequest Body模板:
{ "model": "db-dw-01", "messages": [ { "role": "system", "content": "你是一个SAP MRP专家,只回答与物料需求计划相关的问题。所有数据来自SAP系统,不要编造数字。" }, { "role": "user", "content": "物料A123在工厂1000的库存是{{payload.stock_qty}},计划订单交期是{{payload.bdter}},请用中文总结缺货风险" } ], "temperature": 0.3 }关键的Payload Mapping:
- 从SAP OData响应中提取
d/results/0/stock_qty→ 映射到payload.stock_qty - 将
d/results/0/BDTER时间戳(/Date(1712345678000)/)用Groovy转成yyyy-MM-dd格式 → 映射到payload.bdter
- 从SAP OData响应中提取
Response Parsing:豆包返回的JSON里,答案在
choices/0/message/content,需用XPath提取:/root/choices[1]/message/content/text()
注意:豆包API对
temperature参数敏感。设为0.8时,AI会自由发挥说“建议紧急采购”,但SAP里其实已有采购申请;设为0.3后,回答严格限定在提供的数据范围内,比如“当前库存1250,计划订单交期2024-05-20,无缺货风险”。
4. 实战问题排查:那些文档里绝不会写的坑
4.1 “cc switch local proxy failed while handling codex endpoint /responses”错误溯源
这个错误看似是codex客户端问题,实则是SAP端OData服务的$format参数冲突。现象:codex调用正常,但返回结果里/responses路径报500错误。抓包发现,codex在请求头里加了Accept: application/json; charset=utf-8,而SAP Gateway默认只认application/json。解决方案:
- 进入SAP事务码
SPRO→SAP Reference IMG→SAP NetWeaver→Application Server→Internet Communication Framework→ICM→Maintain ICM Parameters - 找到参数
icm/HTTP/mod_list,添加:icm/HTTP/mod_list = PREFIX=/sap/opu/odata/ PATTERN=$.*\.json$ FILE=/usr/sap/<SID>/SYS/global/icm/json_handler.so - 重启ICM服务:
icman -stop→icman -start
踩坑记录:这个参数修改需要Basis权限,且重启ICM会导致所有OData服务短暂中断。我们选择在凌晨2点操作,提前通知所有AI调用方降级到缓存模式。
4.2 workbuddy技能激活后“no response from backend”问题
表面是网络不通,根源在BTP Destination的Token有效期。现象:技能刚部署时正常,24小时后突然失效。检查CPI日志发现:Invalid OAuth token: expired。原因是BTP默认Token有效期24小时,而workbuddy不会自动刷新。解决方案:
- 在CPI的Destination配置里,勾选
Use Refresh Token - 在BTP cockpit的
Security → Trust Configuration里,为SAP系统启用Refresh Token Grant - 关键:在CPI的OAuth2.0配置中,
Refresh Token URL必须填https://<your-btp>/oauth/token,且Client Authentication选Client Credentials
实操技巧:BTP的Refresh Token机制要求首次获取Token时必须带
scope=openid参数。我们在CPI的OAuth2.0初始请求里,手动在Query Parameters里加scope=openid,否则Refresh Token永远拿不到。
4.3 豆包返回“物料A123不存在”但SAP里明明有数据
这是典型的字符编码问题。现象:CPI日志显示发送给豆包的请求体里material字段是A123,但豆包API返回“未找到物料”。抓SAP OData服务日志发现,实际查询条件是matnr eq 'A123 '(末尾多一个空格)。根源在SAP DDIC结构里,MATNR字段类型是CHAR18,右对齐填充空格。解决方案:
- 在CPI的Message Mapping里,添加
String Function → Trim,对/root/material字段做前后空格去除 - 更彻底的方案:在SEGW的
GET_ENTITYSET方法里,用CONDENSE函数清理:CONDENSE <fs_mdkp>-matnr NO-GAPS.
经验总结:所有SAP CHAR字段在OData暴露时,都必须做
CONDENSE处理。我们后来写了个ABAP报告,批量扫描所有OData服务的DDIC结构,自动插入CONDENSE语句,节省了3个顾问周的工作量。
4.4 并发场景下CPI出现“Connection reset by peer”
这是CPI连接池耗尽的表现。现象:单用户调用正常,10个并发请求时,部分请求返回Connection reset。查CPI监控发现Active Connections峰值达120,超过默认阈值100。解决方案:
- 进入CPI cockpit →
Settings → Runtime Settings - 修改
HTTP Connection Pool Size为200 - 关键:同步调整SAP端SM59的RFC连接池,
Max Connections设为200,Idle Timeout设为300秒
注意:SAP端SM59配置修改后,必须重启SAP Gateway服务(事务码
SICF→ 右键/sap/opu/odata→Restart),否则新连接池不生效。
5. 稳定性加固:生产环境必须做的五件事
5.1 建立SAP端数据校验双保险
AI的输出可信度,取决于输入数据的纯净度。我们强制在SAP端加两道校验:
OData服务层校验:在SEGW的
GET_ENTITYSET方法开头,加入:IF ir_filter->is_empty( ) = abap_true. RAISE EXCEPTION TYPE /iwbep/cx_mgw_busi_exception EXPORTING textid = /iwbep/cx_mgw_busi_exception=>empty_filter. ENDIF.防止AI传空参数导致全表扫描。
CPI层数据清洗:用Groovy脚本验证关键字段:
def material = message.getProperty("material") as String if (!material || material.length() < 1 || material.length() > 18 || material.replaceAll("[A-Z0-9]", "").length() > 0) { throw new Exception("Invalid material number: ${material}") }
5.2 设计分级降级策略
当SAP不可用时,AI不能直接报错,必须有备选方案:
| 故障级别 | 触发条件 | 降级策略 | 用户感知 |
|---|---|---|---|
| L1(轻微) | SAP响应时间>3s | 返回缓存数据+水印“数据截至XX:XX” | 无感知 |
| L2(中度) | 连续3次超时 | 返回预设模板:“当前系统繁忙,已为您预约优先处理” | 轻微延迟 |
| L3(严重) | SAP HTTP 503 | 切换到Excel静态数据源(每周一凌晨自动更新) | 明确提示 |
CPI里用Router组件实现:根据CamelHttpResponseCode和responseTime属性路由到不同分支。
5.3 构建端到端健康检查仪表盘
我们用Grafana搭了一个监控面板,聚合四个数据源:
- CPI Message Monitoring API → 统计成功率、平均延迟
- SAP SM59 → 监控RFC连接池使用率
- SAP DBACOCKPIT → 查询MDKP表最新更新时间(判断MRP是否跑完)
- AI平台日志 → 统计Function Calling调用频次
关键指标告警阈值:
- CPI成功率 < 99.5% → 企业微信告警
- SAP MRP表2小时未更新 → 邮件通知计划员
- codex Function调用失败率 > 5% → 自动触发CPI流程重启
5.4 制定AI输出合规审查清单
所有AI生成内容上线前,必须通过三道审核:
- 字段级审查:确保AI返回的数值与SAP原始数据一致(如库存数±0.1%误差可接受)
- 逻辑级审查:检查AI结论是否符合业务规则(如“缺货”定义必须是
stock_qty < demand_qty,不能只看库存为0) - 法律级审查:财务类输出必须标注“本结果仅供参考,不构成正式会计意见”
我们把这三道审查做成CPI里的Script步骤,自动比对SAP原始数据和AI返回值,不通过则拦截。
5.5 建立版本灰度发布机制
AI模型升级不能全量推送。我们的做法:
- 在CPI里用
Content Filter组件,按用户ID哈希值分流:hash(userId) % 100 < 5→ 走新模型hash(userId) % 100 >= 5→ 走旧模型 - 每2小时统计新旧模型的
业务准确率(人工抽样校验),达标后逐步扩大灰度比例 - 全量发布前,必须完成72小时无故障运行记录
这套机制让我们在最近一次豆包模型升级中,提前发现了新版本对“采购申请”术语的理解偏差,避免了大规模业务误判。
我在实际项目里最深的体会是:所谓“AI连接SAP”,90%的功夫花在SAP侧的服务治理上,而不是AI侧的模型调优。很多团队花大力气调参、换模型,却忽略SAP OData服务的字段裁剪、权限控制、错误码规范这些基础工作。结果就是AI越聪明,出错时越致命。真正靠谱的集成,是让AI像一个谨慎的实习生——它只处理明确授权的数据,只执行清晰定义的动作,所有异常都有预案。现在回头看,标题里写的“codex,workbuddy,豆包”只是表象,背后是同一套SAP服务治理方法论在不同协议下的落地。