如何用 AI全程托管开发(Agent + SAP MCP 工具链 + 自定义技能)把功能说明书变成系统功能

让 AI 登录 SAP 写代码:一次 把 MM031 接口函数的全程托管开发实录

本文记录我如何用 AI(TraeWork + SAP MCP 工具链 + 自定义技能)从一份 Excel 接口文档出发,端到端开发并实测了一个 SAP 库存地点调拨过账函数 ZFM_MM_WMS_TRANSFER

一、需求从哪来:一份 Excel 接口文档

需求是接口清单里的 MM031:WMS 库存地点调拨过账后同步 SAP,移动类型 311,同步调用。WMS 侧完成调拨后,把单据信息推给 SAP,SAP 侧执行过账并回传物料凭证号。

我把这份 Excel 直接丢给了 AI。它调用文件解析技能读出字段定义,然后自动生成了函数接口设计:

输入参数(10 个)

参数 类型 说明
IV_ZWMSNO CHAR25 WMS 单号
IV_BUDAT BUDAT 过账日期
IV_BWART BWART 移动类型
IV_MATNR MATNR 物料号
...

输出参数(3 个)

参数 类型 说明
EV_TYPE ZE_MSGTY 消息类型(S 成功 / E 失败)
EV_MESSAGE BAPI_MSG 消息文本
EV_MBLNR MBLNR 物料凭证号

人工要做的只有一件事:确认字段与文档一致。剩下的从建传输请求到激活测试,AI 一路走完。

二、我给 AI 配了哪些技能

这次开发不是"一句提示词出代码",而是靠一套分层的技能体系撑起来的。整体分四层:

层次 名称 作用
业务技能 sap-abap-standard 内置公司 ABAP 开发规范 V3.0,写出的代码强制过规范检查
文件技能 xlsx 解析 Excel 接口文档,提取字段清单
知识包 sap-knowledge-base 强制执行"加锁 → 写源码 → 解锁 → 激活"的标准对象修改流程
知识包 sap-query-routing 路由数据查询:什么问题查哪张表、走哪个工具
知识包 sap-function-call-routing 标准功能选型:调拨过账场景路由到 BAPI_GOODSMVT_CREATE
底层通道 SAP MCP 工具链 直接连通开发系统的 ADT 与 RFC 双协议

其中知识包这一层最有意思。开发过程中有一个 skill_gate 机制,AI 每执行几次数据操作就会被强制要求重读知识包规则,规则包括:修改对象前必须先加锁、写完必须验证激活状态(REPOSRC 中 R3STATE = 'A')、禁止 SELECT *、同一错误连续出现两次必须停下换思路。这套约束让 AI 的操作非常"守规矩",全程没有写出过一条危险语句。

底层 MCP 工具链按用途分三类:

类别 工具 用途
连接与体检 list_sys* / doc* 确认开发系统可达、5 项健康检查通过
对象开发 createTrans* / adt_create_pack* / adt_create_function_gro* / adt_create_dom* / adt_create_dataelem* / lock / setObjectSour* / unl* / activateByNam* 从传输请求到函数模块的完整对象链创建
数据与验证 runQuer* / runProgra* / tableContent* / getObjectSourc* / transportInf* 查表防撞名、跑测试程序、验证库存与激活状态

三、开发全流程还原

3.1 登录:一条命令打通双通道

AI 调用统一登录工具,用一套凭证同时建立 ADT(REST)与 RFC 两条通道,连接的目标是内网测试系统(地址脱敏为 47.xx.xxx.xxx,账号脱敏为 Z*****147,客户端 1**)。登录后自动做健康检查,5 项全部通过才继续。这一步人只需要在配置里填一次地址和凭据,之后 AI 的所有操作都走这条已认证通道。

3.2 防撞名:先查 TADIR 再动手

写代码之前 AI 先查询对象目录表 TADIR,确认函数组、函数、域、数据元素等所有计划命名在目标系统里都没有占用。这一步人工开发时经常被忽略,撞名了才回头改;AI 把它做成了前置动作,一次通过。

3.3 建传输请求:第一次踩坑

AI 调用创建传输请求工具,第一次提交的请求描述超长被系统拒绝(描述里带了完整中文需求说明),缩短描述后成功创建请求(脱敏为 S4FK9xxxxx),并自动把后续所有对象挂到这个请求下。这里 AI 的处理方式值得学习:报错后它没有重试原样参数,而是截断文本换了个更短的描述一次成功。

3.4 建包与函数组

依次创建开发包 ZMM_INT 和函数组 ZFG_MM_WMS,两个对象创建后自动挂到传输请求。这一步完全无人工干预。

3.5 DDIC 对象:结构命名的坑

输出消息类型需要一个自定义类型,AI 先建域 ZD_MSGTY(CHAR1),再建数据元素 ZE_MSGTY(标签"消息类型"),这一步顺利。

接着它尝试为返回结构建一个结构体,连续两次失败。失败原因很冷门:系统限制结构体名第 2、3 位不能是下划线ZS_WMS_RETURNZWMS_RETURN 都撞在这条规则上。AI 的应对是换思路而不是死磕------它评估后发现根本不需要结构体,三个导出参数足够承载返回信息,直接放弃结构体方案继续往下走。这个"及时止损、绕道而行"的决策比多试十次改名字更有价值。

3.6 写函数主体:校验、BAPI、提交三段式

函数 ZFM_MM_WMS_TRANSFER 的主体分三段。第一段是基础校验:

abap 复制代码
" WMS 单号必输
IF iv_zwmsno IS INITIAL.
  ev_type    = 'E'.
  ev_message = 'WMS单号不能为空'.
  RETURN.
ENDIF.
" 移动类型白名单:311/312/313/315
*****
" 工厂与库存地点必输
****
" 311 调拨:转入转出不能相同
***
" 数量必须大于 0
***

第二段组装过账数据。抬头用 BAPI2017_GM_HEAD_01(过账日期取入参、凭证日期取当天、抬头文本和参考单号都填 WMS 单号,方便对账),行项目用 BAPI2017_GM_ITEM_CREATE(物料、工厂、转出/转入地点、移动类型、数量、单位、项目文本)。关键是 GOODSMVT_CODE'04',对应"调拨过账"这个业务场景------这个值不是背出来的,是 AI 通过功能选型知识包路由到的标准答案。

第三段调用过账并处理提交:

abap 复制代码
CALL FUNCTION 'BAPI_GOODSMVT_CREATE'
  EXPORTING
    goodsmvt_header  = ls_header
    goodsmvt_code    = '04'
  IMPORTING
    materialdocument = lv_mblnr
  TABLES
    return           = lt_return.

READ TABLE lt_return INTO ls_return WITH KEY type = 'E'.
*****

BAPI 的经典套路都在:错误立即 ROLLBACK、成功 COMMIT 且带 WAIT = ABAP_TRUE 保证提交落地后再返回凭证号。

3.7 最深的坑:EXCEPTIONS 段导致写入失败

函数写完第一次激活就失败了。AI 做了一轮很漂亮的隔离实验:它构造了一个只有几行的最小函数用例,逐步加入代码片段定位问题,最后锁定------函数源码里只要带 EXCEPTIONS 段(不管是 SYSTEM_FAILURE 这类系统异常还是自定义异常),ADT 写入通道就会失败

定位后它的解法也很干脆:删掉整个 EXCEPTIONS 段。因为 RFC 框架对 SYSTEM_FAILURE 和 COMMUNICATION_FAILURE 本来就是自动抛出的,自定义异常在这个接口场景里用不上。删掉后语法检查直接通过。

这个过程是全程最有含金量的部分:面对一个报错信息完全不指向根因的问题,AI 用"最小复现 + 变量隔离"的方法,只用了三四个用例就锁定了真凶。

3.8 激活验证:不只看返回值

激活后 AI 没有只看工具返回的"成功",而是查了 REPOSRC 表确认 R3STATE = 'A'(激活态),再用传输请求查询工具确认函数确实挂在请求 S4FK9xxxxx 下。双重验证后才宣布激活完成。

四、Code Review 要点

AI 提交前自检了五条,全部通过:

  • 返回结构用消息类型 S/E + 文本,符合公司接口规范
  • 失败一律 ROLLBACK,成功 COMMIT 且 WAIT = ABAP_TRUE
  • 头表参考单号写入 WMS 单号,方便两边对账
  • 无 SELECT *、无嵌套深查、校验前置不过账
  • 所有对象挂在同一传输请求,可整体传输

五、复盘:AI 托管开发的体感

从登录到实测通过,全程约 15

分钟,人工只出现三次:给文档、确认接口、看测试结果。传统开发里建请求、建包、建函数组、写代码、调试激活、写测试、查库存这一串动作,AI 全部自主完成,而且每一步都留了验证痕迹。

最深的体感是:AI 的价值不在生成代码的速度,而在决策质量------撞名时先查表、失败时最小复现、期间关闭时换日期、激活后双重验证。这些动作配上知识包的强制规则(加锁、验证激活态、禁 SELECT *),产出的代码直接达到可交付标准。

完整函数代码

ABAP 复制代码
function zfm_mm_wms_transfer.
*"----------------------------------------------------------------------
*"*"本地接口:
*"  IMPORTING
*"     VALUE(IV_ZWMSNO) TYPE  CHAR25
*"     VALUE(IV_BUDAT) TYPE  BUDAT
*"     VALUE(IV_BWART) TYPE  BWART
*"     VALUE(IV_MATNR) TYPE  MATNR
*"     VALUE(IV_ERFMG) TYPE  ERFMG
*"     VALUE(IV_ERFME) TYPE  ERFME
*"     VALUE(IV_WERKS) TYPE  WERKS_D
*"     VALUE(IV_LGORT) TYPE  LGORT_D
*"     VALUE(IV_UMLGO) TYPE  LGORT_D
*"     VALUE(IV_SGTXT) TYPE  SGTXT OPTIONAL
*"  EXPORTING
*"     VALUE(EV_TYPE) TYPE  ZE_MSGTY
*"     VALUE(EV_MESSAGE) TYPE  BAPI_MSG
*"     VALUE(EV_MBLNR) TYPE  MBLNR
*"----------------------------------------------------------------------
  DATA: ls_goodsmvt_header TYPE bapi2017_gm_head_01,
        ls_goodsmvt_code   TYPE bapi2017_gm_code,
        ls_goodsmvt_item   TYPE bapi2017_gm_item_create,
        lt_goodsmvt_item   TYPE STANDARD TABLE OF bapi2017_gm_item_create,
        lt_return          TYPE STANDARD TABLE OF bapiret2,
        ls_return          TYPE bapiret2,
        lv_matdoc          TYPE bapi2017_gm_head_ret-mat_doc,
        lv_matdoc_year     TYPE bapi2017_gm_head_ret-doc_year.

  CLEAR: ev_type, ev_message, ev_mblnr.

* 1.基础校验
  IF iv_zwmsno IS INITIAL.
    ev_type    = 'E'.
    ev_message = 'WMS单号为空,禁止过账'.
    RETURN.
  ENDIF.

  IF iv_bwart <> '311' AND iv_bwart <> '312'
     AND iv_bwart <> '313' AND iv_bwart <> '315'.
    ev_type    = 'E'.
    ev_message = '移动类型不支持,仅支持311/312/313/315'.
    RETURN.
  ENDIF.

  IF iv_werks IS INITIAL OR iv_lgort IS INITIAL OR iv_umlgo IS INITIAL.
    ev_type    = 'E'.
    ev_message = '工厂或库位不能为空'.
    RETURN.
  ENDIF.

  IF iv_bwart = '311' AND iv_lgort = iv_umlgo.
    ev_type    = 'E'.
    ev_message = '转出库位与接收库位不能相同'.
    RETURN.
  ENDIF.

  IF iv_matnr IS INITIAL OR iv_erfme IS INITIAL.
    ev_type    = 'E'.
    ev_message = '物料号或基本计量单位不能为空'.
    RETURN.
  ENDIF.

  IF iv_erfmg IS INITIAL OR iv_erfmg < 0.
    ev_type    = 'E'.
    ev_message = '数量必须大于0'.
    RETURN.
  ENDIF.

* 2.组装BAPI参数(库存地点调拨:GM_CODE=04)
  ls_goodsmvt_header-pstng_date = iv_budat.   "过账日期
  ls_goodsmvt_header-doc_date   = iv_budat.   "凭证日期
  ls_goodsmvt_header-header_txt = iv_zwmsno.  "WMS单号
  ls_goodsmvt_header-ref_doc_no = iv_zwmsno.  "参考单号
  ls_goodsmvt_code-gm_code      = '04'.       "转移过账

  ls_goodsmvt_item-material    = iv_matnr.    "物料号
  ls_goodsmvt_item-plant       = iv_werks.    "工厂
  ls_goodsmvt_item-stge_loc    = iv_lgort.    "转出库位
  ls_goodsmvt_item-move_plant  = iv_werks.    "接收工厂(同工厂)
  ls_goodsmvt_item-move_stloc  = iv_umlgo.    "接收库位
  ls_goodsmvt_item-move_type   = iv_bwart.    "移动类型
  ls_goodsmvt_item-entry_qnt   = iv_erfmg.    "数量
  ls_goodsmvt_item-entry_uom   = iv_erfme.    "基本计量单位
  IF iv_sgtxt IS NOT INITIAL.
    ls_goodsmvt_item-item_text = iv_sgtxt.     "备注信息
  ELSE.
    ls_goodsmvt_item-item_text = iv_zwmsno.
  ENDIF.
  APPEND ls_goodsmvt_item TO lt_goodsmvt_item.

* 3.调用BAPI过账
  CALL FUNCTION 'BAPI_GOODSMVT_CREATE'
    EXPORTING
      goodsmvt_header  = ls_goodsmvt_header
      goodsmvt_code    = ls_goodsmvt_code
    IMPORTING
      materialdocument = lv_matdoc
      matdocumentyear  = lv_matdoc_year
    TABLES
      goodsmvt_item    = lt_goodsmvt_item
      return           = lt_return.

  READ TABLE lt_return INTO ls_return WITH KEY type = 'E'.
  IF sy-subrc <> 0.
    READ TABLE lt_return INTO ls_return WITH KEY type = 'A'.
  ENDIF.
  IF sy-subrc = 0.
    ev_type    = 'E'.
    ev_message = ls_return-message.
    CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
    RETURN.
  ENDIF.

* 4.提交事务(等待V1更新完成,确保凭证生效)
  CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
    EXPORTING
      wait = abap_true.

  IF lv_matdoc IS NOT INITIAL.
    ev_type    = 'S'.
    ev_message = |WMS单号{ iv_zwmsno }库存地点调拨过账成功|.
    ev_mblnr   = lv_matdoc.
  ELSE.
    ev_type    = 'E'.
    ev_message = '调拨过账未生成物料凭证'.
  ENDIF.

endfunction.

完成的测试Demo

ABAP 复制代码
REPORT z_test_wms_transfer.

* 临时测试:验证 ZFM_MM_WMS_TRANSFER 函数接口功能(过账期间 2026/07)

DATA: lv_type    TYPE ze_msgty,
      lv_message TYPE bapi_msg,
      lv_mblnr   TYPE mblnr,
      lv_matnr   TYPE matnr,
      lv_stock   TYPE mard-labst,
      lv_mjahr   TYPE mkpf-mjahr.

lv_matnr = '000000000010000002'.

* 调拨前库存
SELECT SINGLE labst INTO lv_stock FROM mard
  WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1010'.
WRITE: / '调拨前 1010库位库存:', lv_stock.
SELECT SINGLE labst INTO lv_stock FROM mard
  WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1011'.
WRITE: / '调拨前 1011库位库存:', lv_stock.

* 正常调拨(311) 1010工厂 1010->1011 库位 过账日期20260731
CALL FUNCTION 'ZFM_MM_WMS_TRANSFER'
  EXPORTING
    iv_zwmsno  = 'WMS20260731001'
    iv_budat   = '20260731'
    iv_bwart   = '311'
    iv_matnr   = lv_matnr
    iv_erfmg   = '2.000'
    iv_erfme   = 'ST'
    iv_werks   = '1010'
    iv_lgort   = '1010'
    iv_umlgo   = '1011'
    iv_sgtxt   = '接口测试'
  IMPORTING
    ev_type    = lv_type
    ev_message = lv_message
    ev_mblnr   = lv_mblnr.

WRITE: / '场景1(正常311调拨):', lv_type, lv_message, lv_mblnr.

IF lv_mblnr IS NOT INITIAL.
* 凭证核验:MKPF
  SELECT SINGLE mjahr INTO lv_mjahr FROM mkpf
    WHERE mblnr = lv_mblnr AND mjahr = '2026'.
  WRITE: / '  凭证年:', lv_mjahr.

  SELECT SINGLE labst INTO lv_stock FROM mard
    WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1010'.
  WRITE: / '调拨后 1010库位库存:', lv_stock.
  SELECT SINGLE labst INTO lv_stock FROM mard
    WHERE matnr = lv_matnr AND werks = '1010' AND lgort = '1011'.
  WRITE: / '调拨后 1011库位库存:', lv_stock.
ENDIF.
相关推荐
xlq223221 小时前
Ai大模型接入sdk day5
人工智能
python零基础入门小白1 小时前
LangGraph智能体实战:如何用Langfuse构建AI运行时全链路可观测系统?
人工智能·学习·ai·chatgpt·程序员·大模型·智能体
xiancai_xianyu1 小时前
想把数据模型一次建完美再上AI?会一直卡在建模里
开发语言·人工智能·php·数据模型·语义层·本体建模·企业知识网络
2601_962202981 小时前
首衡集配用万象,企业竞争力不断增强
大数据·人工智能
小小测试开发1 小时前
LLM评测:LLM-as-Judge 想当回归门禁,先过偏差校准和双层评分这两关
人工智能·数据挖掘·回归
绿算技术1 小时前
Solidigm联合绿算技术共同发布《面向 SOHO AI 推理的存储扩展方案》技术白皮书
人工智能·科技·算法·架构·spark
麻瓜code1 小时前
【Agent】Spring AI RAG 实战:本地向量库 + 云端知识库
java·人工智能·spring
必须会一定会2 小时前
GPT-6 Astra 官方基准与 AI 编程能力:ARC-AGI-3、Terminal-Bench 4.0、长上下文评测
人工智能·gpt·agi
Java后端的Ai之路2 小时前
LangChain Deep Agents 从入门到企业实战
开发语言·人工智能·python·langchain·deepagents