ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

本体驱动AI应用开发:从提示词工程到结构化领域建模的工程化实践

本体驱动AI应用开发:从提示词工程到结构化领域建模的工程化实践

1. 从“提示词工程”到“本体驱动”:为什么我们需要另一种AI应用开发范式

过去一年多,我身边不少做AI应用的朋友都经历过同一种疲惫:花两周时间打磨出一套提示词,模型一升级,效果全变了;或者同一个业务问题,换个模型、换个上下文窗口,输出质量就像坐过山车。大家嘴上说着“提示词工程”,心里其实都清楚,这东西的脆弱程度跟用胶带粘起来的纸箱差不多。

我最初接触“本体驱动开发”这个概念时,第一反应是:这不是知识图谱那一套老东西吗?但真正动手把一个小型业务系统从“纯提示词驱动”改造成“本体驱动”之后,我才意识到这两者的差别,类似于“每次现场即兴演讲”和“先写好剧本再让演员发挥”的差别。前者依赖临场状态,后者有结构兜底。

所谓本体,用最朴素的话说,就是对你业务领域里“有哪些东西、这些东西之间有什么关系、它们能做什么”的一次显式建模。它不是数据库表结构,也不是简单的枚举列表,而是一份机器可读、人类也能看懂的领域契约。而本体驱动的AI原生应用开发,核心思路就是:把这份契约放在系统架构的中心位置,让AI的每一次推理、每一次工具调用、每一次输出校验,都围绕这份契约展开,而不是让模型在自由文本的海洋里裸泳。

这个开源项目(我下面统一称它为“本体驱动开发脚手架”)要解决的问题非常具体:当你需要构建一个需要长期维护、多人协作、且对输出一致性有要求的AI应用时,如何避免把全部逻辑塞进提示词里,如何让领域知识可复用、可测试、可演进。它适合那些已经写过几个AI Demo、被提示词维护折磨过、想要往工程化方向走一步的开发者。如果你还在“写个提示词试试看”的阶段,这篇文章可能会有点重;但如果你已经开始思考“这个AI功能上线三个月后怎么改”,那接下来的内容应该对你有用。

2. 本体驱动开发脚手架的整体设计思路拆解

2.1 为什么把本体放在架构中心,而不是数据库或提示词模板

大多数AI应用的第一版架构是这样的:用户输入 → 拼接提示词 → 调用模型 → 解析输出 → 返回结果。这个链路里,领域知识散落在三个地方:提示词模板里的几句描述、代码里的几个if-else、以及开发者脑子里的隐性知识。项目稍微大一点,这三处就开始互相打架。

本体驱动开发脚手架的做法是把领域知识抽出来,做成一个独立的、有明确Schema的本体层。这个本体层通常包含几个核心要素:实体类型(比如“客户”“订单”“产品”)、关系类型(比如“客户拥有订单”“订单包含产品”)、属性约束(比如“订单金额必须为正数”)、动作定义(比如“取消订单”“修改收货地址”)。这些内容用结构化的方式描述,可以是JSON Schema、RDF/OWL、或者项目自定义的轻量DSL。

为什么不让模型直接读数据库Schema?因为数据库Schema描述的是“数据怎么存”,而本体描述的是“业务怎么理解”。举个例子,数据库里可能有一张user_action_log表,字段是action_type和target_id;但本体里应该描述的是“用户可以收藏商品”“用户可以取消收藏”“收藏和取消收藏是互斥操作”。模型需要的是后者,不是前者。

为什么不用提示词模板承载这些?因为提示词是线性的、易变的、难以做版本对比的。本体是结构化的、可diff的、可单元测试的。你可以给本体写测试用例:给定一个实体和关系,检查推理结果是否符合约束。但你很难给一段提示词写可靠的单元测试。

2.2 三层架构:本体层、编排层、执行层

这个脚手架在架构上分了三层,我按自己的理解重新画一下:

本体层是整个系统的“宪法”。它定义了领域内所有合法的概念、关系、动作和约束。这一层不包含任何模型调用逻辑,纯粹是领域知识的显式表达。项目里通常用一个ontology/目录存放这些定义文件,支持热加载和版本管理。

编排层是“调度中心”。它负责接收用户请求,结合本体层的信息,决定这次请求需要调用哪些工具、按什么顺序调用、每一步的输入输出如何校验。这一层是AI原生应用的核心差异点:它不是硬编码的工作流,而是根据本体中的动作定义和当前上下文,动态生成执行计划。模型在这里的角色是“规划器”,而不是“全能回答者”。

执行层是“手脚”。它包含具体的工具实现,比如查询数据库、调用外部API、执行计算。每个工具在本体层都有对应的动作定义,包括输入参数Schema和输出Schema。执行层不关心业务语义,只负责把输入变成输出。

这三层的分离带来一个直接好处:你可以单独测试本体层的逻辑正确性,单独测试执行层的工具可靠性,而编排层的复杂度被限制在“如何根据本体做规划”这一个问题上。对比之下,纯提示词方案里,业务逻辑、工具调用、输出格式全揉在一段文本里,改一处就可能崩全局。

2.3 与纯提示词方案、传统工作流方案的对比

我整理了一个对比表格,方便你判断这个脚手架是否适合你的场景:

维度纯提示词方案传统工作流方案本体驱动方案
领域知识位置散落在提示词中硬编码在流程节点集中在本体层
修改业务规则改提示词,风险高改代码,需重新部署改本体定义,可热加载
输出一致性依赖模型状态,波动大高,但灵活性差较高,有Schema校验兜底
新增工具在提示词里加描述加节点和连线在本体注册动作,编排层自动感知
测试难度很难写可靠单测可以测流程本体可单测,工具可单测
适合场景快速验证、一次性任务流程固定、步骤明确领域复杂、需长期演进

这个表格不是要证明本体驱动方案全面占优。实际上,如果你只是做一个“把用户输入翻译成SQL”的小工具,纯提示词方案可能半小时就搞定了,上本体驱动反而是过度设计。但如果你要做一个“智能客服工单系统”,涉及用户身份识别、订单查询、退换货规则、升级转接等十几个动作,且这些规则每个月都可能调整,那本体驱动的价值就体现出来了。

3. 核心细节解析与实操要点

3.1 本体定义文件怎么写:从最小可用本体开始

很多人一听到“本体”就想到要建一个覆盖全业务的大而全的模型,然后被工作量吓退。我的经验是:从最小可用本体开始,只定义当前迭代真正需要的实体和动作。这个脚手架支持增量式扩展,你不需要一次性把所有东西都想清楚。

一个最小可用的本体定义通常包含三个文件:

// ontology/entities.json { "entities": { "User": { "description": "系统用户", "properties": { "userId": {"type": "string", "required": true}, "name": {"type": "string", "required": true}, "membershipLevel": {"type": "string", "enum": ["basic", "premium", "vip"]} } }, "Order": { "description": "用户订单", "properties": { "orderId": {"type": "string", "required": true}, "status": {"type": "string", "enum": ["pending", "paid", "shipped", "completed", "cancelled"]}, "amount": {"type": "number", "minimum": 0} } } } }
// ontology/relations.json { "relations": { "UserOwnsOrder": { "from": "User", "to": "Order", "cardinality": "one-to-many", "description": "一个用户拥有多个订单" } } }
// ontology/actions.json { "actions": { "queryOrderStatus": { "description": "查询指定订单的当前状态", "input": { "orderId": {"type": "string", "required": true} }, "output": { "status": {"type": "string"}, "lastUpdated": {"type": "string", "format": "date-time"} }, "constraints": ["orderId must exist in Order entity"] }, "cancelOrder": { "description": "取消一个未发货的订单", "input": { "orderId": {"type": "string", "required": true}, "reason": {"type": "string", "maxLength": 200} }, "output": { "success": {"type": "boolean"}, "message": {"type": "string"} }, "constraints": ["order status must be pending or paid"] } } }

这三个文件加起来不到一百行,但已经足够支撑一个“查订单状态”和“取消订单”的AI助手了。关键在于,这些定义是声明式的,不是命令式的。你不需要写“如果用户问订单状态,就调用queryOrderStatus”,编排层会根据本体中的动作描述和当前对话上下文,自动判断该调用哪个动作。

注意:本体定义里的description字段非常重要,它是模型理解动作语义的主要依据。写描述时要用自然语言把“这个动作做什么、什么时候用、有什么限制”说清楚,不要只写“查询订单”四个字。我一般会写成“当用户询问某个订单当前处于什么状态时使用此动作,需要提供订单号,返回状态和最后更新时间”。

3.2 编排层如何根据本体做动态规划

编排层的核心逻辑可以概括为:把本体中的动作定义转换成模型可理解的工具描述,把当前对话上下文和用户输入一起交给模型,让模型输出一个结构化的执行计划,然后校验这个计划是否符合本体约束。

这个脚手架里,编排层通常包含一个Planner模块。它的工作流程分四步:

第一步,本体加载与工具注册。系统启动时,读取ontology/actions.json,为每个动作生成一个工具描述(通常是OpenAI function calling格式或类似的JSON Schema)。这些工具描述会被注入到模型的系统提示词中。

第二步,上下文组装。把用户当前输入、最近几轮对话历史、以及当前会话中已经确认的实体信息(比如用户已经提供了订单号)组装成一个规划请求。

第三步,模型规划。模型输出一个JSON格式的执行计划,比如:

{ "plan": [ { "action": "queryOrderStatus", "parameters": {"orderId": "ORD-12345"}, "reasoning": "用户询问订单状态,需要先查询" } ] }

第四步,计划校验与执行。编排层拿到计划后,先做本体校验:动作是否存在?参数是否符合Schema?约束是否满足?校验通过后,调用执行层的对应工具,拿到结果,再决定是否需要继续规划下一步。

这个流程的关键在于:模型只负责“选动作”和“填参数”,不负责“编造结果”。结果必须来自执行层的真实工具调用。这就从根本上减少了模型幻觉——模型可以选错动作,但很难凭空编造一个不存在的订单状态,因为状态值必须来自工具返回。

3.3 执行层工具的注册与校验机制

执行层的工具实现可以是任何东西:一个数据库查询函数、一个HTTP请求封装、一个本地计算逻辑。脚手架对工具的要求只有两条:输入输出必须符合本体中定义的动作Schema;工具本身必须是幂等或可回滚的(对于写操作)。

我一般会把工具组织成这样的结构:

# tools/order_tools.py from ontology.registry import action @action("queryOrderStatus") def query_order_status(orderId: str) -> dict: # 实际查询逻辑 order = db.orders.find_one({"orderId": orderId}) if not order: return {"error": "ORDER_NOT_FOUND"} return { "status": order["status"], "lastUpdated": order["updatedAt"].isoformat() } @action("cancelOrder") def cancel_order(orderId: str, reason: str = "") -> dict: order = db.orders.find_one({"orderId": orderId}) if not order: return {"success": False, "message": "订单不存在"} if order["status"] not in ["pending", "paid"]: return {"success": False, "message": "当前状态不可取消"} db.orders.update_one( {"orderId": orderId}, {"$set": {"status": "cancelled", "cancelReason": reason}} ) return {"success": True, "message": "订单已取消"}

@action装饰器的作用是把函数注册到执行层的动作表中,同时绑定本体中定义的动作名。这样编排层在规划时看到的动作列表,和执行层实际能调用的函数,是通过本体这个中间层对齐的。如果本体里定义了一个动作但执行层没有实现,系统启动时就会报错;反过来,执行层实现了但本体没注册的动作,编排层永远不会调用它。

提示:对于写操作,我强烈建议在工具内部再做一次业务规则校验,不要完全依赖编排层的约束检查。因为编排层的校验是基于模型输出的计划,而模型可能被诱导绕过某些约束。工具内部的校验是最后一道防线。

3.4 输出校验与回退策略

即使有了本体约束和工具校验,模型仍然可能输出不符合预期的内容。比如用户问“我的订单什么时候到”,模型可能规划了一个queryOrderStatus动作,但工具返回的状态是shipped,模型在生成最终回复时可能自行添加“预计明天送达”这种工具没有返回的信息。

这个脚手架在输出层加了一道事实一致性校验:最终回复中提到的所有实体属性值,必须能在本次会话的工具调用结果中找到来源。如果模型生成了工具结果中没有的信息,校验层会拦截并触发回退策略。

回退策略通常有三种,按优先级依次尝试:

  1. 重新规划:把校验失败的信息反馈给模型,要求它基于已有工具结果重新生成回复,明确禁止添加未经验证的信息。
  2. 模板化回复:如果重新规划仍然失败,降级为基于工具结果的模板化回复,比如“您的订单当前状态是:已发货。最后更新时间为:2024-01-15 10:30。”
  3. 人工兜底:如果模板化回复也不适用(比如用户问的是开放性问题),则转接人工或提示用户换个问法。

这套机制听起来有点重,但实际跑下来,大部分请求在第一步就能通过。真正触发回退的,往往是那些用户问题本身模糊、或者本体定义覆盖不到的长尾场景。这些场景恰好是你可以用来迭代本体的输入。

4. 实操过程与核心环节实现

4.1 环境准备与项目初始化

这个脚手架对运行环境的要求不高,Python 3.10+ 或 Node.js 18+ 都可以。我以Python版本为例,走一遍初始化流程。

首先克隆项目并安装依赖:

git clone <项目仓库地址> cd ontology-driven-dev python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install -r requirements.txt

依赖里比较关键的是几个包:pydantic用于Schema校验,jsonschema用于本体定义校验,以及一个模型调用SDK(项目支持多种模型后端,通过配置文件切换)。

初始化项目结构:

python -m ontology_dev init --name my-ai-app --template minimal

这个命令会生成一个最小项目骨架:

my-ai-app/ ├── ontology/ │ ├── entities.json │ ├── relations.json │ └── actions.json ├── tools/ │ └── __init__.py ├── config.yaml └── main.py

config.yaml里需要配置模型后端和API密钥(如果你用的是云端模型)。项目也支持本地模型,但本地模型在规划能力上通常弱一些,建议至少用中等规模以上的模型来做编排层的规划。

4.2 定义你的第一个业务本体

假设我们要做一个“会议室预订助手”。先想清楚这个领域里有哪些实体:Room(会议室)、User(用户)、Booking(预订记录)。关系是:用户创建预订,预订关联会议室。动作包括:查询可用会议室、创建预订、取消预订。

entities.json:

{ "entities": { "Room": { "description": "可预订的会议室", "properties": { "roomId": {"type": "string", "required": true}, "name": {"type": "string", "required": true}, "capacity": {"type": "integer", "minimum": 1}, "equipment": {"type": "array", "items": {"type": "string"}} } }, "Booking": { "description": "会议室预订记录", "properties": { "bookingId": {"type": "string", "required": true}, "roomId": {"type": "string", "required": true}, "userId": {"type": "string", "required": true}, "startTime": {"type": "string", "format": "date-time"}, "endTime": {"type": "string", "format": "date-time"}, "status": {"type": "string", "enum": ["confirmed", "cancelled"]} } } } }

actions.json:

{ "actions": { "findAvailableRooms": { "description": "根据时间段和人数要求,查询可用会议室列表", "input": { "startTime": {"type": "string", "format": "date-time", "required": true}, "endTime": {"type": "string", "format": "date-time", "required": true}, "minCapacity": {"type": "integer", "minimum": 1} }, "output": { "rooms": {"type": "array", "items": {"type": "object"}} } }, "createBooking": { "description": "为用户预订指定会议室,需要提供会议室ID、用户ID和时间段", "input": { "roomId": {"type": "string", "required": true}, "userId": {"type": "string", "required": true}, "startTime": {"type": "string", "format": "date-time", "required": true}, "endTime": {"type": "string", "format": "date-time", "required": true} }, "output": { "bookingId": {"type": "string"}, "status": {"type": "string"} }, "constraints": ["endTime must be after startTime", "room must not have overlapping booking"] } } }

写这些定义的时候,我踩过的一个坑是:动作的description写得太简略,导致模型在规划时选错动作。比如findAvailableRooms如果只写“查询会议室”,模型可能会在用户说“帮我订个会议室”时直接调用它,而实际上应该先确认时间和人数。后来我把描述改成“当用户需要查找某个时间段内可用的会议室时使用此动作,需要先确认时间段和人数要求”,规划准确率明显提升。

4.3 实现工具函数并注册到执行层

工具函数的实现要遵循一个原则:输入输出严格对齐本体定义,内部逻辑可以复杂,但对外表现要简单。

# tools/room_tools.py from datetime import datetime from ontology.registry import action from db import rooms_collection, bookings_collection @action("findAvailableRooms") def find_available_rooms(startTime: str, endTime: str, minCapacity: int = 1): start = datetime.fromisoformat(startTime) end = datetime.fromisoformat(endTime) # 找出所有容量达标的会议室 candidate_rooms = list(rooms_collection.find({"capacity": {"$gte": minCapacity}})) # 排除该时间段内已有预订的会议室 overlapping = bookings_collection.find({ "status": "confirmed", "$or": [ {"startTime": {"$lt": end}, "endTime": {"$gt": start}} ] }) booked_room_ids = {b["roomId"] for b in overlapping} available = [r for r in candidate_rooms if r["roomId"] not in booked_room_ids] return { "rooms": [ { "roomId": r["roomId"], "name": r["name"], "capacity": r["capacity"], "equipment": r.get("equipment", []) } for r in available ] } @action("createBooking") def create_booking(roomId: str, userId: str, startTime: str, endTime: str): start = datetime.fromisoformat(startTime) end = datetime.fromisoformat(endTime) if end <= start: return {"error": "INVALID_TIME_RANGE", "message": "结束时间必须晚于开始时间"} # 再次检查冲突(防止并发预订) conflict = bookings_collection.find_one({ "roomId": roomId, "status": "confirmed", "$or": [ {"startTime": {"$lt": end}, "endTime": {"$gt": start}} ] }) if conflict: return {"error": "ROOM_ALREADY_BOOKED", "message": "该时间段会议室已被预订"} booking_id = f"BK-{datetime.now().strftime('%Y%m%d%H%M%S')}" bookings_collection.insert_one({ "bookingId": booking_id, "roomId": roomId, "userId": userId, "startTime": start, "endTime": end, "status": "confirmed" }) return {"bookingId": booking_id, "status": "confirmed"}

注意create_booking里做了两次冲突检查:一次在编排层规划时通过本体约束检查,一次在工具内部执行时再次检查。这不是冗余,而是防御性编程。编排层的检查基于模型输出的计划,可能因为上下文过期而不准确;工具内部的检查基于实时数据库状态,是最终依据。

4.4 跑通第一个端到端请求

启动应用:

python main.py --config config.yaml

然后在交互界面里输入:“帮我找一个明天下午两点到四点、能坐8个人的会议室。”

系统内部的执行流程大致如下:

  1. 编排层收到用户输入,组装规划请求,注入本体中的动作描述。
  2. 模型输出计划:调用findAvailableRooms,参数为startTime=明天14:00、endTime=明天16:00、minCapacity=8。
  3. 编排层校验参数格式,调用执行层的find_available_rooms函数。
  4. 工具返回可用会议室列表。
  5. 编排层把工具结果交给模型,模型生成自然语言回复:“明天下午2点到4点,以下会议室可用:301会议室(容纳10人,有投影仪)、502会议室(容纳8人,有白板)。”
  6. 输出校验层检查回复中的会议室名称和容量是否都能在工具结果中找到,通过后返回给用户。

如果用户接着说“帮我订301”,编排层会结合上下文中的时间段信息,规划createBooking动作,填入roomId=301、userId=当前用户、startTime和endTime从上一轮对话中继承。这就是本体驱动的一个隐性优势:上下文中的实体信息可以被结构化地传递,而不是靠模型从对话历史里“回忆”。

5. 常见问题与排查技巧实录

5.1 模型规划时选错动作或漏填参数

这是最常见的问题,尤其是在本体动作数量超过十个之后。模型面对一长串工具描述,容易混淆相似动作。

排查思路:先看本体中动作的description是否足够区分。如果两个动作的描述都以“查询”开头,模型很容易选错。我的做法是给每个动作的描述加上使用场景前缀,比如“当用户需要取消一个已确认的预订时使用此动作”和“当用户需要修改一个已确认的预订的时间时使用此动作”。场景前缀比动作名称更能帮助模型做区分。

另一个技巧是限制单次规划的动作数量。如果本体中有几十个动作,不要一次性全部注入规划请求。可以根据当前对话的意图分类,先做一层粗筛,只把相关的动作子集交给模型。这个脚手架支持在config.yaml里配置max_actions_per_planning,我一般设为5到8个。

5.2 工具返回结果与本体Schema不一致

有时候工具实现改了,但本体定义没同步更新,导致编排层校验失败。比如工具返回了roomName字段,但本体里定义的是name。

这个问题的根源是本体定义和工具实现之间缺少强制同步机制。我的做法是在CI流程里加一步:启动时自动比对本体中所有动作的输入输出Schema与工具函数的类型注解,不一致就报错。这个脚手架内置了这个检查,但需要你在config.yaml里开启strict_schema_validation: true。

提示:开启严格校验后,开发阶段可能会频繁报错,但这是好事。它强迫你保持本体和实现的一致性,避免上线后才发现字段对不上。

5.3 多轮对话中实体引用丢失

用户说“帮我订301”,这里的“301”是会议室ID,但模型可能不知道。如果上一轮工具返回的会议室列表里有roomId: "301",编排层应该能从上下文中提取这个实体引用。

这个脚手架的做法是维护一个会话实体表:每次工具返回结果后,把结果中的实体ID和名称提取出来,存入当前会话的实体表。下一轮规划时,把实体表作为上下文注入。这样模型看到的不只是对话历史,还有一份结构化的“当前会话已确认实体”列表。

如果实体引用仍然丢失,检查两个地方:一是工具返回结果中的实体字段名是否在本体中有对应的实体定义;二是会话实体表的提取规则是否覆盖了该字段。我一般会在本体中给每个实体加一个displayName属性,用于在会话实体表中做名称匹配。

5.4 输出校验过于严格导致正常回复被拦截

输出校验的初衷是防止幻觉,但如果规则太死,会把一些合理的推理也拦掉。比如用户问“301会议室能坐多少人”,工具返回了capacity: 10,模型回复“301会议室可以容纳10人,适合中型会议”。校验层如果要求回复中每个数字都必须直接来自工具结果,那“中型会议”这个推断就会被拦截。

我的经验是:校验层只校验实体属性值的来源,不校验自然语言推断。具体来说,如果回复中出现了具体的数值、日期、状态值,这些必须能在工具结果中找到;但形容词、建议、总结性表述可以放行。这个边界需要在config.yaml的validation_rules里配置,默认规则是保守的,你可以根据业务容忍度调整。

5.5 常见问题速查表

问题现象可能原因排查动作解决方向
模型选错动作动作描述区分度低检查actions.json中相似动作的description加场景前缀,限制单次规划动作数
参数格式错误模型未按Schema填参查看规划输出中的parameters在动作描述中明确参数格式示例
工具调用失败工具内部异常查看执行层日志工具内部加try-catch,返回结构化错误
输出被校验拦截校验规则过严查看校验失败的具体字段调整validation_rules,区分事实与推断
多轮对话丢实体会话实体表未更新检查工具返回结果的实体提取确保本体中实体有displayName属性
启动时报Schema错误本体与工具不一致比对动作Schema和函数签名开启strict_schema_validation,修复不一致

6. 本体驱动方案的扩展方向与个人实践体会

这个脚手架目前支持的本体定义还比较轻量,适合中小型业务场景。如果你要把它用到更复杂的领域,有几个扩展方向可以考虑。

第一个方向是本体版本管理与迁移。当业务规则变化时,本体定义需要更新,但已经上线的会话可能还在使用旧版本的本体。我试过的一个做法是给本体定义加版本号,编排层在规划时根据会话创建时间选择对应版本的本体。这样新老会话可以并行运行,不会因为本体更新导致老会话突然报错。

第二个方向是本体与向量检索的结合。当前的动作选择完全依赖模型对动作描述的理解。如果动作数量继续增长到几十上百个,可以考虑把动作描述做向量化,先通过语义检索召回候选动作,再交给模型做精细规划。这个脚手架预留了action_retriever接口,可以接入你喜欢的向量库。

第三个方向是本体驱动的测试用例生成。既然本体已经定义了所有合法的实体、关系和动作,理论上可以自动生成覆盖这些组合的测试用例。我目前是手动写测试,但已经在尝试根据本体定义自动生成“给定实体A和动作B,期望输出满足约束C”的测试模板,能省不少重复劳动。

我个人在实际操作中的体会是:本体驱动开发最大的价值不是让AI更聪明,而是让AI的行为更可预测、更可维护。它把“模型会怎么做”这个不确定的问题,转化成了“本体定义得对不对”这个可以工程化解决的问题。模型仍然会犯错,但错误变得可定位、可复现、可修复。对于需要长期运行的AI应用来说,这种可维护性比单次效果的惊艳程度重要得多。

最后分享一个小技巧:如果你不确定某个业务规则该不该放进本体,问自己一个问题——“这条规则如果变了,我需要改几个地方?”如果答案超过一个,那就应该放进本体。因为本体的意义就是让规则只有一个权威来源。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进