给鼎捷易飞 ERP 接一个大模型:我用 ASP.NET Core + DeepSeek 做了个"易飞小智",自然语言直接查业务数据

零改造 ERP、纯只读、可审计。 业务人员问一句"A 客户今年有多少张订单",大模型自动调用易飞 WebApi 取数、汇总结论,全程不写一条数据。本文完整记录架构设计、Function Calling 落地、三条安全红线,以及一个非常反直觉的真实 Bug 排查过程。
在这里插入图片描述

一、为什么要做这件事

制造业上了易飞 ERP 之后,有一个很普遍的场景:

  • 业务:“帮我看下哪些工单逾期未完工?”
  • 物控:打开工单报表 → 设条件 → 导出 Excel → 筛选 → 回复,20 分钟
  • 老板:“C 客户今年接单情况怎么样?” → 又是一轮翻报表。

易飞本身没有内置大模型能力,报表体系强大但门槛高。而大模型最擅长的恰恰是:理解自然语言意图 → 拆解成结构化查询 → 读取数据 → 组织结论。

所以我做了一个独立的 AI 中间层服务 易飞小智(YiFeiErpAI),核心思路一句话:

ERP 只负责业务读写,AI 只负责查询和建议,两者通过标准 WebApi 解耦。

这也决定了它最重要的属性:不动易飞源码、不直连易飞数据库、不新增/修改任何单据。

二、整体架构:一个 ASP.NET Core 服务串起三方

技术栈:ASP.NET Core(.NET 10)+ DeepSeek(OpenAI 兼容协议)+ Function Calling,模型可配置切换 DeepSeek / 通义千问 / 本地 Ollama。

┌─────────────┐   自然语言    ┌──────────────────────────────┐
│  浏览器聊天页 │ ───────────▶ │     易飞小智(ASP.NET Core)    │
│ (纯静态HTML) │ ◀─────────── │                              │
└─────────────┘   结论+表格    │  ChatOrchestrator 工具调用循环 │
                              │  ToolRegistry   工具白名单     │
                              │  ErpClient      鉴权/解包/重试 │
                              │  Sanitizer      出网脱敏       │
                              │  AuditLogger    全程审计       │
                              └───────────┬──────────────────┘
                                           │ HTTP(内网)
                                           ▼
                              ┌──────────────────────────────┐
                              │      YiFeiWebApi(自研)       │
                              │  /api/Copi06/Query 等标准接口  │
                              └───────────────┬──────────────┘
                                              ▼
                                         易飞数据库(只读访问)

几个关键设计决策:

1)AI 不碰数据库,只调 WebApi。 直连库看似简单,但意味着 AI 服务要持有数据库账号、绕过所有业务校验。走 WebApi 则天然继承了易飞侧的授权码、账套隔离、IP 白名单和字段管控。

2)大模型通过 Function Calling 自己决定查什么。 我们把易飞的查询接口注册成 8 个"工具"(客户、供应商、品号信息、品号月档、采购单、工单、销售订单、销售发票/应收),模型理解问题后自己选择工具、生成过滤条件。AI 侧不需要为每种问题写硬编码规则。

3)模型供应商只改配置不改代码。 三家都走 OpenAI 兼容协议:

"Llm": {
  "Provider": "DeepSeek",        // 改成 Ollama / Qwen 即整体切换
  "BaseUrl": "",                 // 留空自动用 DeepSeek 预设
  "Model": "",                   // 留空自动用 deepseek-chat
  "ApiKey": ""                   // 密钥走 user-secrets / 环境变量,绝不入库
}

数据敏感的客户切本地 Ollama 时,把脱敏开关关掉即可,数据不出内网。

三、Function Calling 是怎么落地的

DeepSeek 的工具调用是一个"思考 → 调工具 → 观察结果 → 再思考"的循环。以"哪些工单逾期未完工"为例,模型生成的工具参数是结构化 JSON:

{
  "conditions": [
    { "field_name": "approve_status", "operator": "==", "value": "Y" },
    { "field_name": "status", "operator": "IN", "value": "1,2" },
    { "field_name": "plan_complete_date", "operator": "<", "value": "20260919" }
  ],
  "result_mode": "rows",
  "page_size": 50
}

这个 JSON 被翻译成易飞 WebApi 统一的 StdQuery 契约:

{
  "std_data": {
    "parameter": {
      "page_no": 1,
      "page_size": 50,
      "conditions": {
        "operator": "AND",
        "fields": [
          { "field_name": "approve_status", "operator": "==", "value": "Y" }
        ]
      }
    }
  }
}

这里有一个不能偷懒的关键点:工具的字段必须逐个核对 DTO 白名单。大模型会"自信地编造"字段名,如果不做白名单校验,轻则接口报错,重则 SELECT 出不该暴露的列。我把每个工具可过滤的字段、可用操作符(== / >= / <= / IN / BETWEEN / LIKE / STARTWITH)、可返回的列全部硬编码在工具目录里,模型只能在白名单内发挥。

调用易飞时统一注入三个鉴权头,账套(CompanyId)由服务端配置写死,模型无法指定账套

httpRequest.Headers.TryAddWithoutValidation("X-API-License", _options.LicenseKey);
httpRequest.Headers.TryAddWithoutValidation("X-API-CompanyId", _options.CompanyId);
httpRequest.Headers.TryAddWithoutValidation("X-API-SecurityCode", _options.SecurityCode);

返回结果还做了三层 token 成本控制:

  • 单次工具调用 page_size 硬上限 50 行;
  • "有多少张/总金额"这类问题走 count 模式,只回总数不回明细;
  • 返回列走白名单投影(selectedColumns),银行账号等敏感列从源头不查。

四、三条安全红线,全部在架构上保证

ERP 数据是企业的命根子,做这类项目我的原则是:安全不能靠提示词约束,要靠物理隔离。

风险应对
AI 乱改单据工具目录只注册了 Query,物理上不存在写通道。未来要放开写操作,也必须走"生成草稿 → 人工二次确认"两步式接口
核心数据泄露给公有云大模型出网前脱敏层对手机/身份证/银行卡/邮箱自动掩码;可整组切换本地 Ollama;敏感列不查询
越权访问 / 无法追责复用 WebApi 的授权码+账套+IP 白名单;建议给 AI 单独发只含 query 授权位的密钥;每次问答都记审计日志(谁、哪个账套、问了什么、调了哪些工具、命中多少行、耗时、报错)

前端每条 AI 回复下方还可以展开"查询过程",让用户清楚看到这次结论背后调了哪几个接口、各返回多少条——AI 查了什么,必须可见。

在这里插入图片描述

系统提示词里也明确划定了边界:AI 是顾问不是决策者,数据查不到就如实说,禁止用推测数字替代真实数据

五、实战踩坑:日期查询"全部失败",根因居然和日期无关

这是整个项目最值得分享的一段排查。

上线测试时,所有带日期条件的查询都报"查询失败":

  • “客户 C-00992 今年的订单数” → 销售订单接口 code=-1;
  • “哪些工单逾期” → 工单接口 400 Invalid operation;
  • BETWEEN、>=、甚至 LIKE ‘2026’,全部失败

而不带日期的条件(客户编号、审核状态)全部正常。

怀疑过的错误方向: 日期格式。易飞的日期到底是公历 20260919 还是民国年 1150919?是不是要传 2026-09-19?——最后确认易飞全系统日期统一是 8 位公历 yyyyMMdd,模型传的值完全正确。

真正的突破来自一组对照实验(直接用 Postman 风格脚本打接口):

查询条件返回
order_date == '19990101'(库里肯定没有的日期)code=-1 查询失败
order_date LIKE '2014'(确定有数据)✅ 成功,1443 条
order_date BETWEEN '20140101','20141231'✅ 成功,1443 条
order_date >= '20140101'✅ 成功,17523 条
order_date BETWEEN '2026全年'❌ code=-1

结论瞬间清晰了——日期操作符本身完全正常,问题是"查到 0 行"被当成了失败。 翻后端查询服务的代码,果然:

// 修复前:totalCount == 0 时 Success = false
if (totalCount > 0) {
    // ...分页、填充、投影
    result.Success = true;
} else {
    result.Success = false;   // ← 坑:空结果被标记成失败
}

控制器收到 Success=false 就统一回 code=-1「查询失败」。而这套演示库的销售订单数据恰好只到 2015 年,查"2026 年订单"命中 0 行,于是一个完全正常的业务问题被伪装成了接口故障,连带着让排查方向一路跑偏到日期格式上。

修复也很直接——空结果集是成功的查询,不是失败的查询

// 修复后:0 行同样返回成功(total=0、rows=[])
result.Success = true;
if (totalCount > 0) {
    // ...正常分页
} else {
    result.Data = [];
    result.Rows = [];
}

这个 Bug 的启示:

  1. "查询失败"这类笼统错误信息会严重误导排查,空结果和真异常必须在响应码语义上分开
  2. 排查"某类条件必现失败"的问题,要设计有数据/无数据的对照实验,而不是盯着条件格式猜;
  3. 审计日志里记录模型实际发出的入参和原始报错,是这次能快速定位的关键——没有日志就只能靠猜。

顺带说一句:工单接口那个 400 Invalid operation 是另一个独立问题(多条件组合下动态 LINQ 翻译),排查方法相同:单条件逐个隔离。这也是把查询逻辑收敛到统一 QueryService 的好处——修一处,97 个单据控制器同时受益。

六、为什么聊天页直接内嵌在后端项目里

很多人第一反应是再建一个 Vue 工程。我的选择是 wwwroot 放一个零依赖的单文件 HTML 聊天页

  • 同源访问 /api/chat/ask,没有 CORS 问题;
  • 一个 dotnet run 同时出页面和接口,内网演示/客户试用拷一个目录就行;
  • 自带轻量 Markdown 渲染(AI 回复的明细表直接渲染成表格)、会话 ID 多轮追问、工具轨迹折叠面板、打字动效;
  • 以后要做登录权限、正式产品化,前端搬走即可,后端接口一个字不用改——架构本来就是前后端分离的,只是部署在一起。

七、后续规划(九大方向,按风险从低到高)

易飞 + AI 能做的远不止问答,我按"只读 → 分析 → 生成 → 写操作"排了落地顺序:

  1. ✅ Phase 1 自然语言业务问答(本文,纯只读,已落地);
  2. Phase 2 分析建议:应收账龄简报、工单逾期分析、呆滞料识别、供应商交期评分、安全库存建议——只输出建议,不替代 MRP/APS 运算;
  3. Phase 3 知识库 RAG + AIGC:易飞操作手册问答、月度经营简报/对账单初稿(人工审阅后才能对外);
  4. Phase 4 谨慎放开写:AI 只生成单据草稿,人工二次确认后才调 Create/Approve,独立开关 + 全程审计。

八、底座:YiFeiWebApi 是什么

"易飞小智"能在几周内跑起来,前提是底层有一套标准化的 YiFeiWebApi 中间层。它是面向鼎捷易飞 ERP 自研的 WebApi 服务,目前已覆盖约 100 个单据/档案控制器(采购 PURI、销售 COPI、库存 INVI、工单 MOC、质量 QMS、财务 ACP/ACR/ACT、BOM、CMS 等系列),统一提供:

  • 标准 Query / Read / Create / Update / Delete / Approve 接口,入参出参全部走 StdQuery / StdRead / StdResult 统一契约;
  • 数据库级分页与动态条件下推System.Linq.Dynamic.Core),大表查询只加载一页数据;
  • 授权码机制:按程序+操作粒度授权(如 copi06.query),支持账套隔离、安全码、IP 白名单、有效期校验;
  • AutoMapper 投影、关联数据自动填充、单号自动生成、审核/作废业务规则、Redis 缓存等基础设施。

正是因为有了这套"普通话",接 AI 时要做的只是把标准接口注册成工具,而不是为 AI 重写一遍数据访问层。同样的底座,接企业微信机器人、钉钉 H5、自研报表门户、RPA 平台都是同一套接口。

如果你们也在用鼎捷易飞,受困于"数据在 ERP 里、业务人员取数难",欢迎评论区或私信交流 YiFeiWebApi 的对接经验。

九、源码与交流

  • 技术栈:ASP.NET Core 10 / DeepSeek(OpenAI 兼容)/ Function Calling / Serilog 审计 / 零依赖静态聊天页
  • 完整项目结构与关键代码已在文中给出,建议动手时重点关注:工具字段白名单、page_size 硬上限、账套服务端固定、脱敏与审计、空结果语义这五个护栏。

如果觉得有帮助,点赞 👍 + 收藏 ⭐ + 关注,后续会更新 Phase 2 的分析模板实战(应收账龄、呆滞料、工单超耗根因分析),有易飞 AI 对接问题也欢迎在评论区提问,我会尽量回复。


声明:本文方案定位为业务查询与辅助建议,AI 不直接执行任何 ERP 增删改操作;涉及公有云大模型的环节应做好数据脱敏,核心经营数据建议私有化部署。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐