SAP On-Premise 部署环境下 ABAP 开发对接 AI Agent 方案探讨

一、项目背景与架构约束

1.1 为什么 SAP On-Premise 需要 AI Agent

SAP Joule for Developers 仅对 S/4HANA Cloud Public Edition 开放,ECC 6.0 和 S/4HANA On-Premise 部署的企业无法使用官方 AI 服务。但企业内部数据安全和合规性要求,又使得在企业内网部署 AI Agent 成为必然选择。大量本地部署 SAP 系统的客户,将通过自建 AI Agent 重构 SAP 业务和流程。

1.2 核心架构约束:On-Premise 网络边界

SAP On-Premise 系统通常部署在企业内网,与外部 AI Agent 平台之间存在天然的网络边界。这个约束决定了集成方案的选型:
#mermaid-svg-iGHSiO2ygqKKGZIC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iGHSiO2ygqKKGZIC .error-icon{fill:#552222;}#mermaid-svg-iGHSiO2ygqKKGZIC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iGHSiO2ygqKKGZIC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iGHSiO2ygqKKGZIC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iGHSiO2ygqKKGZIC .marker.cross{stroke:#333333;}#mermaid-svg-iGHSiO2ygqKKGZIC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iGHSiO2ygqKKGZIC p{margin:0;}#mermaid-svg-iGHSiO2ygqKKGZIC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iGHSiO2ygqKKGZIC .cluster-label text{fill:#333;}#mermaid-svg-iGHSiO2ygqKKGZIC .cluster-label span{color:#333;}#mermaid-svg-iGHSiO2ygqKKGZIC .cluster-label span p{background-color:transparent;}#mermaid-svg-iGHSiO2ygqKKGZIC .label text,#mermaid-svg-iGHSiO2ygqKKGZIC span{fill:#333;color:#333;}#mermaid-svg-iGHSiO2ygqKKGZIC .node rect,#mermaid-svg-iGHSiO2ygqKKGZIC .node circle,#mermaid-svg-iGHSiO2ygqKKGZIC .node ellipse,#mermaid-svg-iGHSiO2ygqKKGZIC .node polygon,#mermaid-svg-iGHSiO2ygqKKGZIC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iGHSiO2ygqKKGZIC .rough-node .label text,#mermaid-svg-iGHSiO2ygqKKGZIC .node .label text,#mermaid-svg-iGHSiO2ygqKKGZIC .image-shape .label,#mermaid-svg-iGHSiO2ygqKKGZIC .icon-shape .label{text-anchor:middle;}#mermaid-svg-iGHSiO2ygqKKGZIC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iGHSiO2ygqKKGZIC .rough-node .label,#mermaid-svg-iGHSiO2ygqKKGZIC .node .label,#mermaid-svg-iGHSiO2ygqKKGZIC .image-shape .label,#mermaid-svg-iGHSiO2ygqKKGZIC .icon-shape .label{text-align:center;}#mermaid-svg-iGHSiO2ygqKKGZIC .node.clickable{cursor:pointer;}#mermaid-svg-iGHSiO2ygqKKGZIC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iGHSiO2ygqKKGZIC .arrowheadPath{fill:#333333;}#mermaid-svg-iGHSiO2ygqKKGZIC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iGHSiO2ygqKKGZIC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iGHSiO2ygqKKGZIC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iGHSiO2ygqKKGZIC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iGHSiO2ygqKKGZIC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iGHSiO2ygqKKGZIC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iGHSiO2ygqKKGZIC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iGHSiO2ygqKKGZIC .cluster text{fill:#333;}#mermaid-svg-iGHSiO2ygqKKGZIC .cluster span{color:#333;}#mermaid-svg-iGHSiO2ygqKKGZIC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iGHSiO2ygqKKGZIC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iGHSiO2ygqKKGZIC rect.text{fill:none;stroke-width:0;}#mermaid-svg-iGHSiO2ygqKKGZIC .icon-shape,#mermaid-svg-iGHSiO2ygqKKGZIC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iGHSiO2ygqKKGZIC .icon-shape p,#mermaid-svg-iGHSiO2ygqKKGZIC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iGHSiO2ygqKKGZIC .icon-shape .label rect,#mermaid-svg-iGHSiO2ygqKKGZIC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iGHSiO2ygqKKGZIC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iGHSiO2ygqKKGZIC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iGHSiO2ygqKKGZIC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AI Agent 平台(内网或私有云)
SAP 内网
DMZ 区(防火墙)
ABAP 引擎
HTTPS 44300
反代 + 限流 + 审计
OData V2 REST(唯一协议)
requests / httpx
本地推理
反向代理 / API Gateway
ECC 6.0 / S/4HANA On-Premise
ICF HTTP 服务

/sap/opu/odata(统一 OData 出口)
AI Agent

LangGraph / LangChain
MCP Server

SAP 工具封装
本地 LLM

Qwen / Llama

1.3 三层架构:从 SAP 能力到 Agent 工具

为了让 AI Agent 能调用 SAP 业务操作,我们需要三层架构------每层解耦,各自可替换:
#mermaid-svg-o0fFfcLTssasHE9b{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-o0fFfcLTssasHE9b .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o0fFfcLTssasHE9b .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o0fFfcLTssasHE9b .error-icon{fill:#552222;}#mermaid-svg-o0fFfcLTssasHE9b .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o0fFfcLTssasHE9b .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o0fFfcLTssasHE9b .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o0fFfcLTssasHE9b .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o0fFfcLTssasHE9b .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o0fFfcLTssasHE9b .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o0fFfcLTssasHE9b .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o0fFfcLTssasHE9b .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o0fFfcLTssasHE9b .marker.cross{stroke:#333333;}#mermaid-svg-o0fFfcLTssasHE9b svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o0fFfcLTssasHE9b p{margin:0;}#mermaid-svg-o0fFfcLTssasHE9b .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-o0fFfcLTssasHE9b .cluster-label text{fill:#333;}#mermaid-svg-o0fFfcLTssasHE9b .cluster-label span{color:#333;}#mermaid-svg-o0fFfcLTssasHE9b .cluster-label span p{background-color:transparent;}#mermaid-svg-o0fFfcLTssasHE9b .label text,#mermaid-svg-o0fFfcLTssasHE9b span{fill:#333;color:#333;}#mermaid-svg-o0fFfcLTssasHE9b .node rect,#mermaid-svg-o0fFfcLTssasHE9b .node circle,#mermaid-svg-o0fFfcLTssasHE9b .node ellipse,#mermaid-svg-o0fFfcLTssasHE9b .node polygon,#mermaid-svg-o0fFfcLTssasHE9b .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-o0fFfcLTssasHE9b .rough-node .label text,#mermaid-svg-o0fFfcLTssasHE9b .node .label text,#mermaid-svg-o0fFfcLTssasHE9b .image-shape .label,#mermaid-svg-o0fFfcLTssasHE9b .icon-shape .label{text-anchor:middle;}#mermaid-svg-o0fFfcLTssasHE9b .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-o0fFfcLTssasHE9b .rough-node .label,#mermaid-svg-o0fFfcLTssasHE9b .node .label,#mermaid-svg-o0fFfcLTssasHE9b .image-shape .label,#mermaid-svg-o0fFfcLTssasHE9b .icon-shape .label{text-align:center;}#mermaid-svg-o0fFfcLTssasHE9b .node.clickable{cursor:pointer;}#mermaid-svg-o0fFfcLTssasHE9b .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-o0fFfcLTssasHE9b .arrowheadPath{fill:#333333;}#mermaid-svg-o0fFfcLTssasHE9b .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-o0fFfcLTssasHE9b .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-o0fFfcLTssasHE9b .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-o0fFfcLTssasHE9b .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-o0fFfcLTssasHE9b .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-o0fFfcLTssasHE9b .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-o0fFfcLTssasHE9b .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-o0fFfcLTssasHE9b .cluster text{fill:#333;}#mermaid-svg-o0fFfcLTssasHE9b .cluster span{color:#333;}#mermaid-svg-o0fFfcLTssasHE9b div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-o0fFfcLTssasHE9b .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-o0fFfcLTssasHE9b rect.text{fill:none;stroke-width:0;}#mermaid-svg-o0fFfcLTssasHE9b .icon-shape,#mermaid-svg-o0fFfcLTssasHE9b .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-o0fFfcLTssasHE9b .icon-shape p,#mermaid-svg-o0fFfcLTssasHE9b .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-o0fFfcLTssasHE9b .icon-shape .label rect,#mermaid-svg-o0fFfcLTssasHE9b .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-o0fFfcLTssasHE9b .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-o0fFfcLTssasHE9b .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-o0fFfcLTssasHE9b :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第三层:Agent 工具层
第二层:协议适配层(单一协议)
第一层:SAP 能力暴露层(对外统一 OData)
SAP 标准 OData API

S/4HANA:API_SALES_ORDER_SRV 等
SEGW 自定义 OData 服务

ECC/S/4:封装 RFC / BAPI / LXE
CDS View

@OData.publish / RAP
OData V2 REST 客户端

requests / httpx + CSRF token
MCP Server

@mcp.tool() 封装
LangChain @tool

函数级封装
LangGraph Node

流程节点

分层原则:

  • SAP 能力暴露层 :只在 SAP 系统内部实现,无论底层是标准 API、CDS 还是自定义 RFC,出口统一收口为 OData 服务 (/sap/opu/odata),不关心谁在调用
  • 协议适配层 :纯 Python 实现的单一 OData 客户端,用标准 HTTP 库(requests / httpx)把 OData JSON 转成 Python dict,统一处理 CSRF token、分页、错误
  • Agent 工具层:把适配层的函数包装成 LLM 能理解的工具(带 docstring 和参数 schema),供 LangChain / LangGraph / MCP 注册

1.4 统一协议:为什么对外只暴露 OData

本文所有实践项目遵循一个统一原则:SAP 对外只提供 OData 服务,Agent 端只维护一种协议客户端。

  • 一种协议覆盖所有场景 :S/4HANA 标准业务操作用标准 OData API(api.sap.com 有 700+ 服务);ECC 6.0 和自定义逻辑(如 LXE 翻译、源码读取)用 SEGW(Gateway Service Builder)把 RFC 封装成 OData 服务------SEGW 是"万能适配器",任何 ABAP 逻辑都能包成 EntitySet
  • HTTP 原生 :JSON 报文 + 标准 REST 动词,Python requests 直接调用,无 JVM 依赖(对比 JCo)、无 XML 手工拼装(对比 SOAP RFC)
  • 写操作模式统一 :所有写操作走同一套 CSRF token 流程(GET $metadata fetch → POST/PUT 带回),一处实现,处处复用
  • 批量与原子性 :$batch 支持 change set 内多请求原子提交,适合 Agent 批量写回场景
  • 版本选择:SEGW 产出 OData V2(ECC + S/4HANA 通用),RAP 产出 V4(仅 S/4HANA)。为保证 ECC 兼容性,本文统一使用 V2 语法

二、实践项目:SAP 多语言自动翻译功能

2.1 功能简介

SAP 系统里有大量开发对象的文本元素需要维护多语言翻译,比如事务码(TCODE)短文本、报表的 Text Symbols 和 Selection Texts、类/函数组的接口描述、DDIC Data Element 描述、Domain 固定值、消息类等。传统方案是事务码 SE63 里逐个对象手工切换语言翻译,面对上百个自定义报表、类、事务码时效率极低。

AI Agent 可以利用 SAP 底层翻译函数,绕过 SE63 图形操作界面,实现程序化批量翻译。

2.2 SAP端开发事项

SAP 翻译函数族只能在 ABAP 内部调用,SE63 本身没有对外 API。按统一 OData出口原则,我们写两个自定义 RFC 封装核心逻辑,再用 SEGW 把它们发布为一个 OData 服务,对外只暴露一个 HTTP 端点。

在SAP ABAP栈中具体实现步骤如下:

  1. 自动枚举指定对象类型(TRAN/REPT/CLAS/FUGR/DDIC)中缺失目标语言翻译的对象
  2. 批量读取翻译状态和源语言文本
  3. 调用本地 LLM 生成目标语言翻译
  4. 通过 LXE_OBJ_TEXT_PAIR_WRITE 直接写回翻译透明表
  5. 记录翻译日志,支持纳入传输请求

可翻译对象一览:

对象类型 说明 示例 底层存储表
TRAN 事务码短文本 VA01 中文描述 → "创建销售订单" TSTCT(直接查询)+ LXE 翻译内存
REPT 报表程序 Text Symbols / Selection Texts ZMY_REPORT 的 TEXT-001 / SELECTION-TEXT STXH / STXL
CLAS 类接口元素描述 ZCL_MY_CLASS 方法参数说明
FUGR 函数组接口描述 ZAI_TRANSLATE 函数参数说明
DDIC DDIC 对象 Data Element 描述 / Domain 固定值 / 表字段标签 DD04T / DD07T / DD03T
MSGA T100 消息 Message Class ZAI, 消息号 001 的文本 T100

** ABAP核心翻译函数族**

LXE_OBJ_TEXT_PAIR_READ → 读取单个对象的文本对(源语言 + 当前目标语言)

LXE_OBJ_TEXT_PAIR_READ_TRAN → 批量读取翻译状态(性能优化,预热翻译映射)

LXE_OBJ_TEXT_PAIR_WRITE → 写入翻译(核心,Agent 端 LLM 翻译结果 → SAP)

LXE_OBJ_TRANSLATION_TRANSFER → 将修改添加到传输请求

** 自定义 RFC:批量读取待翻译对象 **

ABAP 复制代码
FUNCTION ZAI_GET_ABAP_TEXTS.
  *"----------------------------------------------------------------------
  *"  IMPORTING
  *"     VALUE(IV_OBJ_TYPE)   TYPE  LXECOBJ-OBJECT  " TRAN/REPT/CLAS/FUGR/DDIC
  *"     VALUE(IV_TARGET_LANG)TYPE  SPRAS           " 目标语言 E=英文 J=日文
  *"     VALUE(IV_SOURCE_LANG)TYPE  SPRAS DEFAULT '1' " 源语言 1=中文
  *"     VALUE(IV_MAX_ROWS)   TYPE  INT4 DEFAULT 100
  *"  TABLES
  *"     ET_ABAP_TEXTS STRUCTURE  ZAI_ABAP_TEXT
  *"  EXCEPTIONS
  *"     NO_DATA_FOUND
  *"     INVALID_OBJECT_TYPE
  *"----------------------------------------------------------------------

  TYPES: BEGIN OF ty_obj_entry,
           obj_name TYPE trobj_name,
         END OF ty_obj_entry.

  DATA: lt_obj_names TYPE TABLE OF ty_obj_entry,
        lt_map_data  TYPE TABLE OF map_t_tstct,
        lv_read_only TYPE as4flag.

  * 1. 根据对象类型枚举待翻译对象(用 SAP 标准表)
  CASE iv_obj_type.
    WHEN 'TRAN'.          "事务码
      SELECT tcode AS obj_name INTO TABLE lt_obj_names
        FROM tstc WHERE tcode LIKE 'Z%' UP TO iv_max_rows ROWS.

    WHEN 'REPT'.          "报表程序
      SELECT obj_name INTO TABLE lt_obj_names
        FROM tadir WHERE object = 'PROG'
          AND obj_name LIKE 'Z%' UP TO iv_max_rows ROWS.

    WHEN 'CLAS'.          "类
      SELECT obj_name INTO TABLE lt_obj_names
        FROM tadir WHERE object = 'CLAS'
          AND obj_name LIKE 'Z%' UP TO iv_max_rows ROWS.

    WHEN 'FUGR'.          * 函数组
      SELECT obj_name INTO TABLE lt_obj_names
        FROM tadir WHERE object = 'FUGR'
          AND obj_name LIKE 'Z%' UP TO iv_max_rows ROWS.

    WHEN OTHERS.
      RAISE invalid_object_type.
  ENDCASE.

  IF lt_obj_names IS INITIAL.
    RAISE no_data_found.
  ENDIF.

  * 2. 批量读取翻译状态(预热翻译映射表)
  lv_read_only = abap_false.
  CALL FUNCTION 'LXE_OBJ_TEXT_PAIR_READ_TRAN'
    EXPORTING
      p_object         = iv_obj_type
      p_read_only      = lv_read_only
    TABLES
      t_obj_name       = lt_obj_names
      map_t_tstct      = lt_map_data
    EXCEPTIONS
      cx_lxe_note      = 1
      OTHERS           = 2.

  * 3. 筛选出目标语言翻译缺失或为空的记录
  LOOP AT lt_map_data INTO DATA(ls_map).
    * 源语言文本必须存在,否则跳过
    IF ls_map-sourcetext IS INITIAL OR ls_map-sourcetext = space.
      CONTINUE.
    ENDIF.
    * 目标语言翻译缺失时输出
    IF ls_map-targettext IS INITIAL OR ls_map-targettext = space.
      APPEND VALUE #(
        obj_type    = iv_obj_type
        obj_name    = ls_map-obj_name
        source_lang = iv_source_lang
        source_text = ls_map-sourcetext
        target_lang = iv_target_lang
      ) TO et_abap_texts.
    ENDIF.
  ENDLOOP.

ENDFUNCTION.

** 自定义 RFC:写入翻译结果 **

ABAP 复制代码
FUNCTION ZAI_SET_ABAP_TEXT.
  *"----------------------------------------------------------------------
  *"  IMPORTING
  *"     VALUE(IV_OBJ_TYPE)   TYPE  LXECOBJ-OBJECT    " TRAN/REPT/CLAS/FUGR/DDIC
  *"     VALUE(IV_OBJ_NAME)   TYPE  TROBJ_NAME        " 对象名,如 VA01 / ZMY_REPORT
  *"     VALUE(IV_TARGET_LANG)TYPE  SPRAS              " 目标语言 E=英文 J=日文
  *"     VALUE(IV_SOURCE_LANG)TYPE  SPRAS DEFAULT '1'  " 源语言 1=中文
  *"     VALUE(IV_TRANSLATED) TYPE  STRING             " LLM 生成的翻译文本
  *"  EXPORTING
  *"     VALUE(EV_STATUS)    TYPE  LXESTATPRC        " 返回状态 S=成功 F=失败 W=警告
  *"     VALUE(EV_MESSAGE)    TYPE  STRING
  *"----------------------------------------------------------------------

  DATA: lt_text_pair TYPE TABLE OF lxet_text_pair,
        ls_text_pair TYPE lxet_text_pair.

  * 构建 LXE_OBJ_TEXT_PAIR_WRITE 所需的 T_TEXT_PAIR 参数
  * 注意:LXE 函数需要目标语言的文本对结构,TEXT 字段长度根据对象类型不同
  ls_text_pair-text = iv_translated.
  APPEND ls_text_pair TO lt_text_pair.

  * 调用 SAP 核心翻译写入函数
  CALL FUNCTION 'LXE_OBJ_TEXT_PAIR_WRITE'
    EXPORTING
      p_object    = iv_obj_type
      p_obj_name  = iv_obj_name
      p_source    = iv_source_lang
      p_lang      = iv_target_lang
      t_text_pair = lt_text_pair
    IMPORTING
      p_status    = ev_status
    EXCEPTIONS
      OTHERS      = 1.

  IF sy-subrc <> 0.
    ev_status = 'F'.
    ev_message = |SY-SUBRC = {sy-subrc}|.
  ELSEIF ev_status = 'S'.
    ev_message = |翻译成功:{iv_obj_type}/{iv_obj_name} → {iv_target_lang}|.
  ELSEIF ev_status = 'W'.
    ev_message = |警告:部分成功 {iv_obj_type}/{iv_obj_name} ({ev_status})|.
  ELSE.
    ev_message = |失败:{iv_obj_type}/{iv_obj_name} 状态={ev_status}|.
  ENDIF.

ENDFUNCTION.

** SEGW 封装:把 RFC 发布为OData 服务 **

把 RFC 发布为统一 OData 服务 ZAI_TRANSLATION_SRV

复制代码
SEGW 项目 ZAI_TRANSLATION_SRV
├── EntityType:AbapText(主键:ObjType + ObjName + TargetLang)
│   ├── ObjType        Edm.String   TRAN/REPT/CLAS/FUGR/DDIC(Filterable)
│   ├── ObjName        Edm.String   对象名,如 VA01(Filterable)
│   ├── SourceLang     Edm.String   源语言 1=中文(Filterable)
│   ├── SourceText     Edm.String   源文本(Query 返回)
│   ├── TargetLang     Edm.String   目标语言 E=英文(Filterable)
│   ├── TranslatedText Edm.String   LLM 翻译结果(Create 输入)
│   ├── Status         Edm.String   P_STATUS:S=成功 W=警告 F=失败
│   └── Message        Edm.String   结果消息
└── EntitySet:TextPairs

操作映射:

OData 操作 RFC 函数 参数映射
GET /TextPairs?$filter=ObjType eq 'TRAN' and TargetLang eq 'E'&$top=50 ZAI_GET_ABAP_TEXTS ObjType→IV_OBJ_TYPE,TargetLang→IV_TARGET_LANG,SourceLang→IV_SOURCE_LANG,$top→IV_MAX_ROWS
POST /TextPairs ZAI_SET_ABAP_TEXT 实体属性 → RFC Import 参数(ObjType/ObjName/SourceLang/TargetLang/TranslatedText)

SEGW 里的三个关键步骤:

  1. Data Model :定义上面的 EntityType(或 Import from RFC 自动生成),把查询字段勾选 Filterable
  2. Service Implementation :Query 行映射 ZAI_GET_ABAP_TEXTS,Create 行映射 ZAI_SET_ABAP_TEXT(在 Mapping Details 里把实体属性拖到 RFC 参数)
  3. Generate Runtime + 激活 ,再到 /IWFND/MAINT_SERVICE 添加服务(System Alias 选 LOCAL,嵌入式网关场景)

2.3 AI Agent 平台开发事项

Agent 端只用一个 OData 客户端访问 SAP(统一协议),封装成 MCP Server,再用 LangGraph 编排翻译流程。

协议适配层:SAP OData 客户端

python 复制代码
import os
import requests

SAP_HOST = "s4hana.example.com"
SAP_PORT = "44300"
SAP_CLIENT = "100"
SAP_USER = "AI_TRANSLATOR"
SAP_PASSWORD = os.getenv("SAP_PASSWORD")

class SapODataClient:
    """SAP OData V2 统一客户端:Query / Read / Create / Function Import

    统一处理:
    - Basic Auth + sap-client
    - 写操作 CSRF token(GET $metadata fetch → POST/PUT 带回)
    - $format=json(V2 默认返回 Atom XML,必须显式要 JSON)
    """

    def __init__(self, service_path: str):
        self.base = f"https://{SAP_HOST}:{SAP_PORT}{service_path}"
        self.session = requests.Session()
        self.session.auth = (SAP_USER, SAP_PASSWORD)
        self.session.headers.update({
            "sap-client": SAP_CLIENT,
            "Accept": "application/json",
        })
        self._csrf_token = None

    def _fetch_csrf_token(self) -> str:
        """SAP 网关强制:任何写操作前必须先 Fetch CSRF token"""
        resp = self.session.get(f"{self.base}/$metadata",
                                headers={"X-CSRF-Token": "Fetch"})
        resp.raise_for_status()
        self._csrf_token = resp.headers.get("x-csrf-token", "")
        return self._csrf_token

    def query(self, entity_set: str, filters: str = None,
              top: int = 50, skip: int = None) -> list[dict]:
        """GET EntitySet:$filter / $top / $skip 分页查询"""
        params = {"$format": "json"}
        if filters:
            params["$filter"] = filters
        if top:
            params["$top"] = str(top)
        if skip:
            params["$skip"] = str(skip)
        resp = self.session.get(f"{self.base}/{entity_set}", params=params)
        resp.raise_for_status()
        return resp.json().get("d", {}).get("results", [])

    def read(self, entity_set: str, key: str) -> dict:
        """GET 单实体。key 如:ObjType='TRAN',ObjName='VA01',TargetLang='E'"""
        resp = self.session.get(f"{self.base}/{entity_set}({key})")
        resp.raise_for_status()
        return resp.json().get("d", {})

    def create(self, entity_set: str, payload: dict) -> dict:
        """POST 实体:自动带 CSRF token,支持 Deep Insert 嵌套"""
        if self._csrf_token is None:
            self._fetch_csrf_token()
        resp = self.session.post(
            f"{self.base}/{entity_set}",
            json=payload,
            headers={"X-CSRF-Token": self._csrf_token,
                     "Content-Type": "application/json"})
        resp.raise_for_status()
        return resp.json().get("d", {})

    def function_import(self, name: str, params: dict) -> list:
        """OData V2 Function Import(带参数的查询操作)"""
        params = {**params, "$format": "json"}
        resp = self.session.get(f"{self.base}/{name}", params=params)
        resp.raise_for_status()
        data = resp.json().get("d", [])
        if isinstance(data, dict):     # 部分网关版本包一层 results
            data = data.get("results", [])
        return data

MCP Server:SAP ABAP 对象翻译工具封装

python 复制代码
from fastmcp import FastMCP

mcp = FastMCP("SAP ABAP Translation MCP")
sap = SapODataClient("/sap/opu/odata/sap/ZAI_TRANSLATION_SRV")

@mcp.tool()
def get_abap_objects_for_translation(
    obj_type: str = "TRAN",
    target_language: str = "E",
    source_language: str = "1",
    max_rows: int = 50,
) -> list[dict]:
    """从 SAP 获取指定类型中缺失目标语言翻译的 ABAP 开发对象列表。
    返回每个对象的名称和源语言文本,供 Agent 批量翻译。

    Args:
        obj_type: 对象类型,可选值:
            TRAN = 事务码短文本
            REPT = 报表程序 Text Symbols / Selection Texts
            CLAS = 类接口描述
            FUGR = 函数组接口描述
            DDIC = DDIC Data Element / Domain 描述
        target_language: 目标语言代码,E=英文, J=日文, F=法文, G=德文
        source_language: 源语言代码,1=中文, D=德文, J=日文
        max_rows: 最多返回的对象数量
    """
    filt = (f"ObjType eq '{obj_type}' and TargetLang eq '{target_language}' "
            f"and SourceLang eq '{source_language}'")
    return sap.query("TextPairs", filters=filt, top=max_rows)

@mcp.tool()
def set_abap_text_translation(
    obj_type: str,
    obj_name: str,
    target_language: str,
    translated_text: str,
    source_language: str = "1",
) -> str:
    """将 LLM 生成的翻译写回 SAP 系统。
    OData Create → 后端 RFC → LXE_OBJ_TEXT_PAIR_WRITE,效果等价于 SE63 手工翻译。

    Args:
        obj_type: 对象类型(TRAN/REPT/CLAS/FUGR/DDIC)
        obj_name: 对象名称,如 VA01(事务码)、ZMY_REPORT(报表)
        target_language: 目标语言代码
        translated_text: LLM 翻译生成的目标语言文本
        source_language: 源语言代码,默认 1=中文
    """
    result = sap.create("TextPairs", {
        "ObjType": obj_type,
        "ObjName": obj_name,
        "SourceLang": source_language,
        "TargetLang": target_language,
        "TranslatedText": translated_text,
    })
    status = result.get("Status", "F")
    message = result.get("Message", "")
    if status == "S":
        return f"{obj_type}/{obj_name} → {target_language} 翻译成功"
    elif status == "W":
        return f"{obj_type}/{obj_name} → {target_language} 警告: {message}"
    else:
        return f"{obj_type}/{obj_name} → {target_language} 失败: {message}"

@mcp.tool()
def get_abap_text_pair(obj_type: str, obj_name: str, target_language: str) -> dict:
    """读取单个 ABAP 对象的翻译状态:源语言文本 + 当前目标语言文本。
    用于翻译前的"预读"和翻译后的"回读校验"。

    Args:
        obj_type: 对象类型(TRAN/REPT/CLAS/FUGR/DDIC)
        obj_name: 对象名称
        target_language: 目标语言代码
    """
    key = (f"ObjType='{obj_type}',ObjName='{obj_name}',"
           f"TargetLang='{target_language}'")
    return sap.read("TextPairs", key)

if __name__ == "__main__":
    mcp.run()

LangGraph Agent 执行流
#mermaid-svg-eVSaX3dCGrQZ4lx5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .error-icon{fill:#552222;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .marker.cross{stroke:#333333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 p{margin:0;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .cluster-label text{fill:#333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .cluster-label span{color:#333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .cluster-label span p{background-color:transparent;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .label text,#mermaid-svg-eVSaX3dCGrQZ4lx5 span{fill:#333;color:#333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .node rect,#mermaid-svg-eVSaX3dCGrQZ4lx5 .node circle,#mermaid-svg-eVSaX3dCGrQZ4lx5 .node ellipse,#mermaid-svg-eVSaX3dCGrQZ4lx5 .node polygon,#mermaid-svg-eVSaX3dCGrQZ4lx5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .rough-node .label text,#mermaid-svg-eVSaX3dCGrQZ4lx5 .node .label text,#mermaid-svg-eVSaX3dCGrQZ4lx5 .image-shape .label,#mermaid-svg-eVSaX3dCGrQZ4lx5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .rough-node .label,#mermaid-svg-eVSaX3dCGrQZ4lx5 .node .label,#mermaid-svg-eVSaX3dCGrQZ4lx5 .image-shape .label,#mermaid-svg-eVSaX3dCGrQZ4lx5 .icon-shape .label{text-align:center;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .node.clickable{cursor:pointer;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .arrowheadPath{fill:#333333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eVSaX3dCGrQZ4lx5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eVSaX3dCGrQZ4lx5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eVSaX3dCGrQZ4lx5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .cluster text{fill:#333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .cluster span{color:#333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eVSaX3dCGrQZ4lx5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .icon-shape,#mermaid-svg-eVSaX3dCGrQZ4lx5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .icon-shape p,#mermaid-svg-eVSaX3dCGrQZ4lx5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .icon-shape .label rect,#mermaid-svg-eVSaX3dCGrQZ4lx5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eVSaX3dCGrQZ4lx5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eVSaX3dCGrQZ4lx5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eVSaX3dCGrQZ4lx5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} START
get_abap_objects_for_translation

OData Query 枚举待翻译对象
LLM 批量翻译

生成目标语言文本
set_abap_text_translation

OData Create → LXE 写回
P_STATUS 状态校验

S=成功 W=警告 F=失败
END

Agent 的核心翻译逻辑

python 复制代码
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated
import operator

class TranslationState(TypedDict):
    obj_type: str
    target_lang: str
    source_lang: str
    objects: list[dict]                      # 待翻译对象列表
    translations: Annotated[list, operator.add]  # 翻译结果累积
    errors: list[str]

def fetch_objects(state: TranslationState) -> dict:
    """从 SAP 获取指定类型中缺失目标语言翻译的对象"""
    result = mcp_client.call_tool("get_abap_objects_for_translation", {
        "obj_type": state["obj_type"],
        "target_language": state["target_lang"],
        "source_language": state["source_lang"],
        "max_rows": 20,
    })
    return {"objects": result}

def translate_and_write(state: TranslationState) -> dict:
    """LLM 翻译 + 写回 SAP(每次处理一个对象,循环执行)"""
    done = len(state["translations"])
    if done >= len(state["objects"]):
        return {"translations": []}

    obj = state["objects"][done]
    obj_type = obj["OBJ_TYPE"]
    obj_name = obj["OBJ_NAME"]
    source_text = obj.get("SOURCE_TEXT", "")

    if not source_text or source_text == " ":
        return {"translations": [{"obj": obj_name, "skipped": True,
                                   "reason": "源文本为空"}]}

    # 本地 LLM 翻译
    translated = llm.invoke(
        f"Translate this SAP ABAP object text to English. "
        f"Keep it concise and consistent with SAP terminology:\n"
        f"Source (Chinese): {source_text}"
    ).content.strip()

    # 写回 SAP(OData Create → 后端 RFC → LXE_OBJ_TEXT_PAIR_WRITE)
    write_result = mcp_client.call_tool("set_abap_text_translation", {
        "obj_type": obj_type,
        "obj_name": obj_name,
        "target_language": state["target_lang"],
        "source_language": state["source_lang"],
        "translated_text": translated,
    })

    return {"translations": [{
        "obj": obj_name,
        "source": source_text,
        "translated": translated,
        "sap_result": write_result,
    }]}

# 图编排
graph = StateGraph(TranslationState)
graph.add_node("fetch", fetch_objects)
graph.add_node("translate", translate_and_write)
graph.add_edge(START, "fetch")
graph.add_edge("fetch", "translate")
graph.add_conditional_edges("translate",
    lambda s: END if len(s["translations"]) >= len(s["objects"]) else "translate",
    {"translate": "translate", END: END})

agent = graph.compile()

# 启动:翻译事务码(TRAN)从中文(1)到英文(E)
result = agent.invoke({
    "obj_type": "TRAN",
    "target_lang": "E",
    "source_lang": "1",
    "objects": [],
    "translations": [],
    "errors": [],
})
相关推荐
百数平台1 小时前
百数照片知识库配置指南:图片上传、OCR 识别与智能 / 人工标注全流程说明
低代码·ai·ocr
程序人生8881 小时前
从“把隐私文件上传到别人云端“到本地可控:DocConverter Web 立项初心与架构选型
前端·图像处理·人工智能·opencv·架构·ocr·文心一言
数智工坊1 小时前
视觉SLAM第11讲|回环检测:词袋模型、字典构建与相似度计算全解析
人工智能·深度学习·线性代数
TechEdu2026061 小时前
[人工智能]Python11:NumPy 源代码、软件架构与软件流程
人工智能·numpy
loulanyue_1 小时前
突破单点AI瓶颈:慧博零售全域Agent的数智化实践——读慧博科技CTO贾世龙2026云栖专场演讲
人工智能·科技·零售
百数平台1 小时前
百数表单知识库配置指南:对接低代码业务表单、字段自动映射与实时同步全说明
低代码·ai
蜗牛互联网1 小时前
Java HttpClient 调用 Gemini 图像理解与金额字段校验
java·开发语言·人工智能·后端·python
开开心心就好1 小时前
视频里的图片怎么提取?双击一下就导出
java·前端·人工智能·spring·智能手机·intellij-idea·excel
蜗牛互联网1 小时前
Python Responses API视觉输入与本地金额校验最小实现
java·开发语言·人工智能·后端·python