☰
实践:本地大模型Agent通过自定义MCP服务对接SAP OData(二)——TaoToken统一Key打通调用链
2026/10/18 6:56:23 网站建设 项目流程

1. 从一次 SAP 表结构查询说起:多工具调用时 Key 分散到底有多痛

本地大模型 Agent 接 SAP OData 这件事,真正跑通第一版之后,问题往往不在“能不能查”,而在“查得稳不稳、换环境麻不麻烦”。我最初的做法很直接:在 MCP Server 里写死 SAP 的 Basic Auth,在 Agent 侧写死本地推理服务的 api_key,两边各管各的。单机跑一个 demo 没问题,但只要多接一个工具、多换一台机器,Key 就开始满天飞。

具体痛点有三个。第一是鉴权入口分散:SAP OData 用 HTTPBasicAuth,本地 LLM 用 OpenAI 兼容的 api_key,MCP Server 自己如果还要调外部模型做意图补全,又是第三个 Key。第二是环境切换成本高:测试机、开发机、演示机三套地址,每次都要改代码里的常量,改漏一处就是 401。第三是排障困难:Agent 报错只说“工具调用失败”,你根本不知道是 MCP 没起来、SAP 拒绝、还是模型侧 Key 失效。

这一篇要解决的就是这个“调用链上的 Key 统一”问题。核心思路是:把模型侧和工具侧的外部调用统一收敛到 TaoToken 的 API 通道,用一个 Key 覆盖 OpenAI 兼容接口,MCP Server 内部对 SAP 的访问保持独立配置,但通过环境变量注入,不再硬编码。这样 Agent 侧只认一个 Base URL 和一个 Key,MCP 侧只认 SAP 的账号密码,职责清晰。

适合谁看:已经在用 Qwen-Agent 或类似框架跑本地 Agent、手里有 FastMCP 写的自定义工具、并且需要对接 SAP OData 这类企业接口的开发者。如果你还没搭过 MCP Server,建议先看第一篇把 stdio 拉起流程跑通,这一篇重点在“统一 Key + 端到端验证”。

我试过把三套 Key 都塞进一个.env里,结果 Agent 子进程读不到父进程的环境变量,排查了半天。后来改成显式传参 + 统一 API 通道,才稳定下来。下面按可复制的顺序展开。

2. TaoToken 前置准备:统一 Key 与 API 通道怎么接

在动手改 MCP Server 之前,先把模型侧的调用通道统一掉。TaoToken 提供的是 OpenAI 兼容接口,也就是说你原来写openai客户端的地方,只需要改base_url和api_key两个字段,其余调用方式不变。这对 Agent 场景很关键,因为 Qwen-Agent 的llm_cfg本身就是按 OpenAI 兼容格式组织的。

先明确三个东西:

  • Base URL:https://taotoken.net/api,注意这是 API 根路径,不带任何多余后缀。
  • API Key:在控制台的 API Keys 页面创建,格式通常是sk-开头的一串。
  • Model ID:填你在模型列表里看到的实际模型名,比如Qwen3.6-35B-A3B这类,不要自己编。

创建 Key 的入口在这里:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。进去之后新建一个 Key,复制出来先存到本地环境变量,别直接写进代码。

为什么要在 Agent 侧做这层统一?因为本地 Agent 经常会挂多个工具,其中一部分工具(比如做文本摘要、做字段映射)需要回调模型。如果每个工具各自配一套模型地址和 Key,调用链就会碎掉。统一到 TaoToken 之后,Agent 主循环和工具内部的模型调用共用同一个通道,排障时只需要看一个出口。

配置方式我推荐用环境变量,而不是写死在llm_cfg里。这样换机器时只改 shell 配置,不动代码。下面这段是 Agent 侧的读取逻辑,先建立这个习惯:

import os TAOTOKEN_BASE_URL = os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api") TAOTOKEN_API_KEY = os.getenv("TAOTOKEN_API_KEY", "") if not TAOTOKEN_API_KEY: raise RuntimeError("TAOTOKEN_API_KEY 未设置,请先导出环境变量")

对应的 shell 导出命令(Linux/macOS):

export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_API_KEY="sk-你的实际Key"

Windows PowerShell 用:

$env:TAOTOKEN_BASE_URL="https://taotoken.net/api" $env:TAOTOKEN_API_KEY="sk-你的实际Key"

这里有个容易踩的坑:Base URL 末尾不要加/v1。OpenAI 官方 SDK 会自动拼/chat/completions,如果你写成https://taotoken.net/api/v1,最终路径会变成/api/v1/chat/completions,而正确的应该是/api/chat/completions。我一开始就是多加了/v1,报了一晚上的 404,后来把路径打日志才看出来。

另外,MCP Server 作为子进程被 Agent 拉起时,默认继承父进程环境变量。但如果你用的是某些 IDE 的调试配置,环境变量可能不会透传。稳妥做法是在 MCP Server 启动脚本里显式读取,读不到就报错退出,而不是静默用默认值。这样问题会暴露在启动阶段,而不是等到工具调用时才炸。

统一 Key 之后,Agent 侧的所有模型请求都走这一个出口。接下来才是 MCP Server 内部对 SAP 的封装,那部分保持独立,因为 SAP 的鉴权体系和企业内网绑定,不适合也不应该走外部通道。

3. 可复制配置:MCP Server 封装 SAP OData 与 Agent 接入片段

这一节给可直接复制的配置和代码。分三块:SAP 服务封装、FastMCP Server 注册工具、Agent 侧llm_cfg与mcpServers配置。

先看 SAP 服务封装。核心是用requests.Session保持连接,sap-client走 URL 参数而不是 Header,字符串主键用单引号包裹。这些细节在 OData 里很关键,写错就是 400。

# SAP_Service.py import os import requests from urllib.parse import quote from requests.auth import HTTPBasicAuth session = requests.Session() BASE_URL = os.getenv( "SAP_ODATA_BASE_URL", "http://192.168.31.21:8080/sap/opu/odata/sap/YSAP_SERVICE_SRV/" ) SAP_CLIENT = os.getenv("SAP_CLIENT", "800") session.auth = HTTPBasicAuth( os.getenv("SAP_USERNAME", "SAP_XXXX"), os.getenv("SAP_PASSWORD", "") ) session.headers.update({"Accept": "application/json"}) def get_table_data(table_name: str): """按主键表名读取表数据""" key = quote(str(table_name), safe="") url = f"{BASE_URL}DB_DATASet('{key}')?$format=json&sap-client={SAP_CLIENT}" resp = session.get(url, timeout=30) resp.raise_for_status() return resp.json().get("d") def get_table_info(table_name: str): """按主键表名读取表结构信息""" key = quote(str(table_name), safe="") url = f"{BASE_URL}DB_INFOSet('{key}')?$format=json&sap-client={SAP_CLIENT}" resp = session.get(url, timeout=30) resp.raise_for_status() return resp.json().get("d")

注意SAP_PASSWORD没有给默认值,读不到就是空字符串,SAP 会直接返回 401。这是故意的,避免用假密码跑出一堆看不懂的错误。

接着是 FastMCP Server。工具注册用@mcp.tool(),docstring 就是 Agent 判断何时调用的唯一依据,必须写清楚“读什么、参数是什么、返回什么格式”。

# SAPMCPServer.py import logging from fastmcp import FastMCP from SAP_Service import get_table_data, get_table_info logging.basicConfig( level=logging.INFO, 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,返回JSON格式结果""" 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表的结构定义信息,用于了解字段含义,传入表名如 T001""" 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 侧配置。这里同时体现 TaoToken 统一 Key 和 MCP Server 的 stdio 拉起。command必须指向装了fastmcp的那个 Python 解释器绝对路径,不能写python3,否则会用到系统 Python 而找不到依赖。

# agent_run.py import os from qwen_agent.agents import Assistant llm_cfg = { "model": os.getenv("TAOTOKEN_MODEL_ID", "Qwen3.6-35B-A3B"), "model_server": os.getenv("TAOTOKEN_BASE_URL", "https://taotoken.net/api"), "api_key": os.getenv("TAOTOKEN_API_KEY", ""), "generate_cfg": { "use_raw_api": True, "extra_body": {"chat_template_kwargs": {"enable_thinking": True}}, }, } tools = [{ "mcpServers": { "SAPMCPServer": { "command": "/path/to/.venv/bin/python3", "args": ["/path/to/SAPMCPServer.py"], } } }] bot = Assistant(llm=llm_cfg, function_list=tools) prompt = "我想知道SAP会计科目表 T030 的表结构,以及前10行数据,请展示。" 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="")

如果你用的是 Cline 或 Claude Code 这类工具,配置形态类似,关键是三件套齐全:Base URL 填https://taotoken.net/api,Key 填 TaoToken 的 Key,Model ID 填实际模型名。Cline 的 MCP 配置里同样把command指向虚拟环境解释器。

这里给一个 JSON 形态的配置片段,方便直接贴到支持 JSON 配置的客户端里:

{ "mcpServers": { "SAPMCPServer": { "command": "/path/to/.venv/bin/python3", "args": ["/path/to/SAPMCPServer.py"], "env": { "SAP_ODATA_BASE_URL": "http://192.168.31.21:8080/sap/opu/odata/sap/YSAP_SERVICE_SRV/", "SAP_CLIENT": "800", "SAP_USERNAME": "SAP_XXXX", "SAP_PASSWORD": "你的SAP密码" } } } }

注意env块里放的是 SAP 侧凭据,不是 TaoToken 的 Key。TaoToken 的 Key 属于 Agent 主进程,不要混进 MCP Server 的 env,否则职责就乱了。这个边界要守住。

4. 验证请求:一次真实查询跑通端到端调用链

配置写完,必须用一次真实查询验证整条链路。验证的目标不是“能出结果”,而是“每一段都按预期工作”:Agent 发现工具、MCP Server 被拉起、SAP OData 返回数据、模型整理输出。

第一步,单独验证 SAP OData 通不通。先绕过 Agent,直接跑SAP_Service.py里的函数:

cd /path/to/project export SAP_ODATA_BASE_URL="http://192.168.31.21:8080/sap/opu/odata/sap/YSAP_SERVICE_SRV/" export SAP_CLIENT="800" export SAP_USERNAME="SAP_XXXX" export SAP_PASSWORD="你的SAP密码" python3 -c " from SAP_Service import get_table_info import json print(json.dumps(get_table_info('T030'), ensure_ascii=False)[:500]) "

如果这一步返回了字段列表,说明 SAP 侧鉴权和 OData 路径都对。如果报 401,检查用户名密码;报 404,检查BASE_URL和sap-client;报 400,多半是主键引号或$format的问题。

第二步,单独验证 MCP Server 能启动并注册工具。用 MCP 自带的 CLI 检查:

/path/to/.venv/bin/python3 -m fastmcp inspect SAPMCPServer.py

正常会列出read_table_data和read_table_info两个工具,以及它们的参数 schema。如果这里报ModuleNotFoundError,说明解释器路径不对,回到上一节检查command。

第三步,跑 Agent 端到端。执行agent_run.py,观察输出。预期会看到类似这样的过程:

工具调用: SAPMCPServer-read_table_info 工具返回: {"MANDT": "Client", "KTOPL": "Chart of Accounts", ...} 工具调用: SAPMCPServer-read_table_data 工具返回: {"results": [{"MANDT": "800", "KTOPL": "ACIA", ...}]} 助手: 以下是 T030 表的字段说明和前 10 行数据...

这里有个细节:Qwen-Agent 注册 MCP 工具后,工具名会自动拼接为服务名-工具名,也就是SAPMCPServer-read_table_info。这是为了避免多个 MCP Server 之间的工具名冲突。你在日志里看到这个前缀是正常的,不要以为是配置错了。

第四步,验证 TaoToken 通道确实被用上。在 Agent 侧加一行日志,打印实际请求的model_server:

print(f"模型通道: {llm_cfg['model_server']}, 模型: {llm_cfg['model']}")

输出应该是https://taotoken.net/api和你设置的模型名。如果这里显示的是localhost:8000,说明环境变量没生效,Agent 还在用旧的本地地址。

实测下来,整条链路第一次跑通大概需要 3 到 5 轮工具调用。前两轮模型在确认表结构,第三轮开始读数据,后面几轮在整理输出。如果轮次特别多(比如超过 50 轮),通常是返回的数据量太大,模型在反复处理。这时候要在 MCP 工具里加过滤或分页,而不是让模型硬扛全表数据。

验证通过的标准:Agent 最终输出里包含 T030 的字段说明和至少 10 行数据,且日志里能看到两次工具调用记录。达到这个状态,说明统一 Key 打通调用链的目标达成了。

5. 本篇常见错排查:401、local proxy failed 与 reading choices 报错

这一节按真实报错来。我把调试过程中遇到的几类问题整理出来,对照着看能省不少时间。

第一类:401 Unauthorized。这个最直接,但来源可能有两个。一个是 SAP 侧,用户名密码错或sap-client不对;另一个是 TaoToken 侧,Key 没设置或写错。区分方法看报错位置:如果错误发生在SAP_Service.py的raise_for_status(),是 SAP 侧;如果发生在 Agent 的模型请求阶段,是 TaoToken 侧。SAP 侧检查SAP_USERNAME、SAP_PASSWORD、SAP_CLIENT三个环境变量;TaoToken 侧检查TAOTOKEN_API_KEY是否导出成功,可以用echo $TAOTOKEN_API_KEY确认。

第二类:local proxy failed或连接被拒绝。这个通常出现在 Agent 尝试连模型通道时。原因可能是 Base URL 写错,比如多加了/v1或少了/api。正确写法是https://taotoken.net/api。另一个可能是本机网络策略限制了外部请求,需要确认当前环境能正常访问该地址。注意不要用任何非官方的网络工具去“绕过”,那既不合规也会让问题更难排查。直接在终端用curl测一下:

curl -s -o /dev/null -w "%{http_code}" https://taotoken.net/api

返回 200 或 401 都说明通道可达,返回 000 说明连不上。

第三类:Error reading choices或choices字段为空。这个报错来自 OpenAI 兼容响应解析。常见原因是模型返回了非标准结构,或者model字段填了一个不存在的模型名。检查TAOTOKEN_MODEL_ID是否和模型列表里的一致。另一个原因是use_raw_api没开,导致 Qwen-Agent 对响应做了额外包装。在generate_cfg里保持use_raw_api: True。

第四类:MCP Server 启动后 Agent 找不到工具。表现是 Agent 说“没有可用工具”或直接不调用。检查三点:command是否是虚拟环境解释器的绝对路径;args里的脚本路径是否存在;MCP Server 启动时有没有报错退出。可以手动执行command和args组合的命令,看能否正常启动。如果启动就报ModuleNotFoundError: No module named 'fastmcp',说明解释器不对。

第五类:OAuth 相关报错。如果你在配置里看到OAuth字样,通常是因为客户端把 MCP Server 当成了需要 OAuth 的远程服务。stdio 模式下不需要 OAuth,检查配置里有没有多余的type或url字段。stdio 只需要command和args,不要混入streamable-http的配置项。

第六类:工具调用成功但返回空。这个多半是 SAP OData 的d字段结构和你预期不一致。有些服务返回{"d": {"results": [...]}},有些返回{"d": {...}}。在SAP_Service.py里打印原始响应确认结构,再决定取哪一层。不要假设所有 OData 服务返回格式一样。

排障时建议把日志分级:Agent 侧只保留 WARNING 以上,MCP Server 侧详细日志写入mcp_server.log。这样终端输出干净,出问题时直接看日志文件。stdio 模式下 MCP Server 的 stdout 会被协议占用,业务日志只能写文件,这一点务必注意,否则日志会污染协议通信导致 Agent 解析失败。

6. 把统一 Key 用在长期编码与 Agent 任务上

调用链打通之后,下一步是让它稳定服务于日常任务。这里的关键是把 TaoToken 的 Key 管理从“临时导出”变成“长期可用”。如果你只是偶尔跑一次 demo,环境变量手动导出就够了;但如果你要把这个 Agent 用在日常的 SAP 数据查询、报表生成、字段映射这类重复任务上,建议走 Coding Plan 这类长期方案,把 Key 和额度管理集中起来,避免每次换机器都要重新配。

入口在这里:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它的价值在于把模型调用通道固定下来,Agent 侧只需要读环境变量,不用关心 Key 的轮换和额度。

对于需要频繁验证模型输出的场景,比如调 docstring 措辞、调 prompt 让 Agent 更准确地选择工具,可以用模型对话页面快速试:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。把 MCP 工具的 docstring 贴进去,让模型判断“什么情况下该调用这个工具”,比反复跑完整 Agent 快得多。

如果你要接 Claude Code 这类编码工具,配置形态和本篇的 Agent 侧一致,三件套还是 Base URL、Key、Model ID。文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各客户端的接入示例。API Keys 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。

回到 SAP 集成本身,后续可以做的方向有几个。一是给 OData 请求加过滤和分页,避免全表返回导致模型处理轮次过多;二是增加 POST、PUT、DELETE 支持,让 Agent 不只是读数据;三是在 MCP Server 里加一层字段映射缓存,减少重复的表结构查询。这些都不需要改 Agent 主循环,只需要在 MCP 工具层扩展。

最后提醒一点:MCP Server 不要直连生产库。SAP OData 服务本身应该走企业既定的接口层,MCP 只是调用方。把凭据放在环境变量里,不要提交到代码仓库。统一 Key 解决的是调用链清晰度问题,不是权限问题,权限边界还是要靠 SAP 侧的账号体系来守。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询