Skip to main content

AI-native PLM system for managing parts, BOMs, and documents

Project description

BOMX — AI-native PLM Agent

用自然语言管理零件和 BOM 的工程工具。
本文档面向学习者,重点解释"为什么这样设计"和"代码里值得学的地方"。


目录

  1. 项目是什么
  2. 技术栈选型
  3. 整体架构
  4. 目录结构详解
  5. 数据模型
  6. 后端详解(FastAPI)
  7. AI Agent 详解
  8. 前端详解(Vue 3)
  9. 值得学习的设计模式
  10. 快速启动
  11. API 一览

1. 项目是什么

BOMX 是一个 PLM(产品生命周期管理) 工具,核心功能:

  • 管理零件(Part):编码、名称、类型、多版本
  • 管理BOM(Bill of Materials,物料清单):多级树状结构,支持草稿/发布状态
  • 管理图文档:图纸、工艺、手册等文件,可关联到 BOM 行
  • AI 驱动:通过自然语言对话完成以上所有操作(CLI + Web 双入口)

三种使用方式

用户
 ├── CLI(终端)  ──→  plm.py  ──→  LiteLLM + Tools  ──→  FastAPI
 ├── Web UI      ──→  Vue 3   ──→  /api/*            ──→  FastAPI
 └── Web AI 助手 ──→  Vue 3   ──→  /api/chat (SSE)   ──→  LiteLLM + Tools

2. 技术栈选型

后端

用途 学习重点
FastAPI Web 框架 异步路由、依赖注入、Pydantic 自动校验
SQLAlchemy 2.0 ORM async session、mapped_column 新语法、关系加载
aiosqlite SQLite 异步驱动 单机开发无需 PostgreSQL
Pydantic v2 数据校验/序列化 BaseModel、字段校验、from_attributes
LiteLLM LLM 多模型适配层 统一调用 OpenAI / Claude / Qwen,Tool Use 模式
python-multipart 文件上传解析 Form + File 同时接收

前端

用途 学习重点
Vue 3 UI 框架 Composition API、<script setup>、ref/computed
Element Plus 组件库 el-table、el-dialog、el-tree、el-tabs
Vite 构建工具 dev proxy、HMR、TypeScript 支持
Axios HTTP 客户端 与 fetch 的对比;本项目 AI 聊天改用原生 fetch 读 SSE
Vue Router 路由 createWebHistory、动态路由参数

CLI Agent

用途
Typer 构建命令行工具,自动生成 --help
Rich 终端彩色输出、表格、树状图、Markdown 渲染
httpx 同步 HTTP 客户端(agent 调用后端 API)

3. 整体架构

┌─────────────────────────────────────────────────────────┐
│                      用户界面层                          │
│  ┌───────────────┐        ┌────────────────────────┐    │
│  │  CLI (plm.py) │        │   Web UI (Vue 3 :5173) │    │
│  │  Typer + Rich │        │   Element Plus         │    │
│  └──────┬────────┘        └───────────┬────────────┘    │
└─────────┼───────────────────────────────┼────────────────┘
          │ 自然语言                       │ REST / SSE
          ▼                               ▼
┌─────────────────────────────────────────────────────────┐
│                  AI Agent 层                             │
│  ┌──────────────────────────────────────────────────┐   │
│  │  LiteLLM  ←→  Tool Use Loop                      │   │
│  │  (Qwen / Claude / GPT 均可)                      │   │
│  │  工具: search_part / get_bom / create_part / ...  │   │
│  └──────────────────────┬───────────────────────────┘   │
└─────────────────────────┼───────────────────────────────┘
                          │ HTTP (httpx / fetch)
                          ▼
┌─────────────────────────────────────────────────────────┐
│               FastAPI 后端层 (:8001)                     │
│  ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐  │
│  │  /parts  │ │  /bom    │ │/documents │ │  /chat   │  │
│  └────┬─────┘ └────┬─────┘ └─────┬─────┘ └────┬─────┘  │
│       └────────────┴─────────────┴─────────────┘        │
│                    SQLAlchemy ORM (async)                │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
                    SQLite / bomx.db
                 (~/.plm/files/ 存文件)

关键设计决策

为什么 Agent 工具调用自己的 HTTP 接口,而不是直接调 DB?
保持关注点分离:Agent 层只知道"后端有哪些 API",不关心数据库细节。这样 Agent 可以脱离后端单独测试,也方便未来指向远程服务器。

为什么 Web AI 聊天用 SSE 而不是 WebSocket?
SSE(Server-Sent Events)是单向流,只需要普通 HTTP,不需要握手协议,浏览器原生支持。工具调用进度推送场景天然适合 SSE。WebSocket 适合双向实时通信(如多人协作),此处过度设计。


4. 目录结构详解

bomx/
│
├── app/                        # FastAPI 后端
│   ├── main.py                 # 应用入口:注册路由、数据库初始化
│   │
│   ├── models/                 # SQLAlchemy ORM 模型(数据库表结构)
│   │   ├── base.py             # 公共 Mixin:UUID主键、创建/更新时间
│   │   ├── part.py             # 零件:Part + PartVersion(主版本模式)
│   │   ├── bom.py              # BOM:BomHeader + BomLine + BomLineDocument
│   │   ├── document.py         # 文档:Document + DocumentVersion + FileObject
│   │   ├── user.py             # 用户(占位,认证暂未实现)
│   │   └── audit.py            # 审计日志(SQLAlchemy event hook 自动记录)
│   │
│   ├── routers/                # FastAPI 路由处理器(Controller 层)
│   │   ├── part.py             # /api/parts — 零件 CRUD
│   │   ├── bom.py              # /api/bom   — BOM 增删改查、发布
│   │   ├── document.py         # /api/bom/lines/{id}/documents — BOM行附件
│   │   ├── documents.py        # /api/documents — 全局文档库(含独立上传)
│   │   └── chat.py             # /api/chat  — AI 对话(SSE 流式)
│   │
│   └── schemas/                # Pydantic 模型(请求体/响应体定义)
│       ├── part.py             # PartCreate、PartResponse 等
│       ├── bom.py              # BomHeaderCreate、BomLineResponse 等
│       └── document.py         # BomLineDocumentResponse 等
│
├── agent/                      # CLI AI Agent
│   ├── loop.py                 # REPL 主循环:读输入 → LLM → 工具 → 输出
│   ├── tools.py                # 工具定义(LiteLLM schema)+ 工具执行函数
│   ├── api.py                  # httpx 封装:调用后端 REST API
│   └── display.py              # Rich 渲染:零件表格、BOM 树状图
│
├── web/                        # Vue 3 前端
│   └── src/
│       ├── App.vue             # 应用外壳:顶栏、侧边栏、主内容区(CSS Grid)
│       ├── style.css           # 全局设计系统(CSS 变量、工具类)
│       ├── main.ts             # Vue 应用入口
│       │
│       ├── api/                # 前端 HTTP 客户端(axios 封装)
│       │   ├── part.ts
│       │   ├── bom.ts
│       │   └── document.ts
│       │
│       ├── components/         # 全局组件
│       │   ├── AIDrawer.vue    # ⌘I AI 聊天抽屉(SSE 流式 + 历史记录)
│       │   └── CommandPalette.vue  # ⌘K 命令面板
│       │
│       ├── views/              # 页面组件
│       │   ├── PartList.vue    # 零件列表(搜索、过滤、新建)
│       │   ├── PartDetail.vue  # 零件详情(版本管理)
│       │   ├── BomList.vue     # BOM 列表(新建向导)
│       │   ├── BomDetail.vue   # BOM 详情(可拖拽分栏、多级树、文档关联)
│       │   └── DocumentList.vue # 文档库(上传、预览、下载)
│       │
│       └── router/index.ts     # 路由配置
│
├── database.py                 # 数据库连接:引擎创建、session 工厂
├── plm.py                      # CLI 入口:Typer app,读取 .env
├── seed_examples.py            # 示例数据:智能手环 + 电动自行车两个产品
├── seed.py                     # 最小种子:系统用户
│
├── start.bat                   # Windows:同时启动后端 + 前端
├── stop.bat                    # Windows:按窗口标题关闭服务
└── restart.bat                 # Windows:stop + start

5. 数据模型

核心概念:主记录 + 版本记录

项目采用"主记录不可变,变更产生版本"的模式:

Part (身份)  ──< PartVersion (历史版本)
  code             version: "A", "B", "1.0"
  name             is_current: true/false
  part_type        current_version_id ──┐
  └──────────────────────────────────── ┘ 快速查当前版

这是工业 PLM 系统的标准设计,好处是:

  • 零件的"身份"(编码)永远不变
  • 每次改设计参数,产生新版本,旧版本完整保留
  • BOM 可以固定引用某个版本(child_ver_rule = "fixed"

完整 ER 关系

Part ──────────────< PartVersion
  │                      │
  │                      └──── BomHeader ──────< BomLine
  │                                                 │
  │                                        child_part_id (→ Part)
  │                                                 │
  │                                           BomLineDocument
  │                                                 │
  └────── PartDocument                         Document
                │                                   │
                └──────────────────────────── DocumentVersion
                                                    │
                                               FileObject
                                          (MD5去重,物理文件)

多级 BOM 的实现方式

每个总成零件有自己独立的 BomHeader。多级结构通过递归查询实现:

查询 BAND-001 的BOM树:
  1. 找 BAND-001 的 BomHeader → 得到直属子件列表
  2. 对每个子件,检查它是否也有 BomHeader(即它是否也是总成)
  3. 如果是,递归获取其 BomHeader
  4. 组装成树(DFS,Set 防循环)

6. 后端详解(FastAPI)

异步数据库 Session 管理

database.py 是理解异步 SQLAlchemy 的核心:

# 创建异步引擎
engine = create_async_engine(DATABASE_URL, echo=False)

# Session 工厂
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)

# 依赖注入函数(每个请求获得独立 session)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSessionLocal() as session:
        async with session.begin():   # 自动提交/回滚
            yield session

FastAPI 依赖注入用法:

@router.get("/parts")
async def list_parts(db: AsyncSession = Depends(get_db)):
    # db 由框架自动注入,请求结束后自动关闭

Pydantic + SQLAlchemy 分层

项目严格分离了两种模型:

文件位置 作用
ORM 模型 app/models/ 定义数据库表结构,SQLAlchemy 使用
Schema 模型 app/schemas/ 定义 API 请求/响应格式,Pydantic 使用

ORM 模型转 Pydantic 靠 model_config = {"from_attributes": True},让 Pydantic 可以直接读 SQLAlchemy 对象的属性。

文件去重:MD5 内容寻址

app/routers/documents.py 中的 _get_or_create_file_object()

md5 = hashlib.md5(data).hexdigest()
existing = await db.scalar(select(FileObject).where(FileObject.md5 == md5))
if existing:
    return existing  # 相同内容不重复存储
# 否则写入磁盘 → ~/.plm/files/{md5[:2]}/{md5}/{原文件名}

这是内容寻址存储(Content-Addressable Storage)的简化版,Git 也用同样思路。

审计日志:SQLAlchemy Event Hook

app/models/audit.py 注册了 after_flush 事件,无需在每个接口里手写日志:

@event.listens_for(Session, "after_flush")
def _after_flush(session, flush_context):
    for obj in session.new:      # 新增的对象
        _log(session, obj, "create")
    for obj in session.dirty:    # 修改的对象
        _log(session, obj, "update", 记录字段变化)
    for obj in session.deleted:  # 删除的对象
        _log(session, obj, "delete")

SSE 流式响应

app/routers/chat.py 展示了如何用 FastAPI 发送 SSE:

def generate():          # 普通生成器函数
    yield "data: {...}\n\n"   # SSE 格式:data: + JSON + 双换行
    yield "data: {...}\n\n"

return StreamingResponse(generate(), media_type="text/event-stream")

StreamingResponse 接收生成器,FastAPI 把它放入线程池运行(因为是同步 def),边计算边推送。


7. AI Agent 详解

Tool Use(工具调用)模式

这是项目的核心 AI 技术,流程:

用户输入
    │
    ▼
LLM(带工具定义)
    │
    ├── 直接回答? ──→ 输出文本,结束
    │
    └── 需要调工具?
            │
            ▼
        解析 tool_calls(LLM 告诉我们调哪个工具、传什么参数)
            │
            ▼
        execute_tool()(Python 代码真正调用 API)
            │
            ▼
        把工具结果塞回 history
            │
            ▼
        再次调用 LLM ──→ 循环,直到 LLM 不再调工具

agent/tools.py 里,每个工具有两部分:

# 1. Schema(告诉 LLM 这个工具能做什么、参数是什么)
{
    "type": "function",
    "function": {
        "name": "search_part",
        "description": "按编码或名称搜索零件",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "搜索关键词"}
            },
            "required": ["query"]
        }
    }
}

# 2. 执行函数(真正干活的 Python 代码)
def _search_part(query: str, base_url: str = None) -> str:
    parts = api.list_parts(query=query, base_url=base_url)
    return f"找到 {len(parts)} 个零件:..."

LiteLLM 的价值

# 调用 Claude
litellm.completion(model="claude-sonnet-4-6", messages=..., tools=...)

# 调用 Qwen(只改 model 和 api_base,代码完全一样)
litellm.completion(model="openai/qwen-plus", api_base="https://...", messages=..., tools=...)

LiteLLM 统一了不同 LLM 厂商的 API 差异,项目切换模型只需改 .env 里的 PLM_MODEL

history 的结构

LLM 对话历史是一个消息列表,工具调用时结构较复杂:

history = [
    {"role": "user", "content": "查看手环的BOM"},
    # LLM 决定调工具,返回这样的消息:
    {"role": "assistant", "content": "", "tool_calls": [
        {"id": "call_xxx", "function": {"name": "get_bom", "arguments": '{"part_code":"BAND-001"}'}}
    ]},
    # 工具执行结果,必须用 tool_call_id 对应:
    {"role": "tool", "tool_call_id": "call_xxx", "content": "BOM已渲染,直属子件6个..."},
    # LLM 看到工具结果后,给出最终回答:
    {"role": "assistant", "content": "BAND-001 手环的BOM包含6个直属子件..."}
]

8. 前端详解(Vue 3)

Composition API + <script setup>

项目全部使用 Vue 3 最新写法:

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

// ref: 响应式基本值
const loading = ref(false)
const parts = ref<Part[]>([])

// computed: 依赖 parts 自动重算
const activeParts = computed(() => parts.value.filter(p => p.status === 'active'))

// 生命周期
onMounted(() => load())

async function load() { ... }
</script>

相比 Options API(data(), methods:, computed:),Composition API 把相关逻辑组织在一起,更适合复杂组件。

CSS Grid 应用外壳

App.vue 用 CSS Grid 实现固定布局(不出现双滚动条的关键):

.bx-app {
  display: grid;
  grid-template-columns: 220px 1fr;    /* 侧边栏 | 主内容 */
  grid-template-rows: 52px 1fr 28px;  /* 顶栏 | 内容 | 底栏 */
  height: 100vh;
  overflow: hidden;  /* 整体不滚动 */
}

.bx-main {
  overflow-y: auto;  /* 只有主内容区滚动 */
}

BomDetail 的可拖拽分栏

BomDetail.vue 实现了鼠标拖拽调整左右面板宽度:

function startResize(e: MouseEvent) {
  resizing.value = true
  const startX = e.clientX
  const startW = treeWidth.value

  function onMove(e: MouseEvent) {
    treeWidth.value = Math.min(600, Math.max(220, startW + (e.clientX - startX)))
  }
  function onUp() {
    resizing.value = false
    window.removeEventListener('mousemove', onMove)
    window.removeEventListener('mouseup', onUp)
  }
  window.addEventListener('mousemove', onMove)
  window.addEventListener('mouseup', onUp)
}

核心思路:记录起始鼠标位置和起始宽度,在 mousemove 里计算差值,注意在 mouseup 时清除监听器防止内存泄漏。

SSE 流式接收(AIDrawer)

fetch 原生支持流式读取,比 EventSource 更灵活(支持 POST):

const res = await fetch('/api/chat', { method: 'POST', body: ... })
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''

while (true) {
  const { done, value } = await reader.read()
  if (done) break

  buffer += decoder.decode(value, { stream: true })
  const lines = buffer.split('\n')
  buffer = lines.pop() ?? ''  // 保留不完整的行,等下次 read()

  for (const line of lines) {
    if (line.startsWith('data: ')) {
      const event = JSON.parse(line.slice(6))
      // 处理事件...
    }
  }
}

buffer = lines.pop() 是处理 TCP 分包的关键:一次 read() 可能只收到半行,需要缓存等待后续数据。

API 层封装

web/src/api/ 目录把所有 HTTP 调用集中管理:

// part.ts
const http = axios.create({ baseURL: '/api' })

export const partApi = {
  list: (params?) => http.get<Part[]>('/parts', { params }).then(r => r.data),
  get: (code) => http.get<PartDetail>(`/parts/${code}`).then(r => r.data),
  create: (data) => http.post<Part>('/parts', data).then(r => r.data),
}

好处:所有接口地址集中在一处,修改时不用找遍所有 Vue 文件。


9. 值得学习的设计模式

1. 主记录 + 版本记录(Master-Version Pattern)

Part → PartVersionDocument → DocumentVersion。工业软件的经典模式,保留完整历史,current_version_id 外键提供快速查当前版本的能力。

2. 依赖注入(FastAPI Depends)

每个路由函数通过 Depends(get_db) 自动获得数据库 session,框架负责生命周期管理。这是控制反转(IoC)思想的实践。

3. 内容寻址存储(Content-Addressable Storage)

文件按 MD5 存储,相同内容只存一份。Git 对象库、Docker 镜像层都用同样思路。

4. SSE for Server Push

后端用生成器函数 yield 事件,前端用 ReadableStream 读取。比轮询高效,比 WebSocket 简单,适合"服务端单向推送进度"场景。

5. LLM Tool Use 循环

"LLM → 调工具 → 结果回填 → 再次 LLM"的循环模式,是 AI Agent 的基础架构。理解这个循环,就理解了 ChatGPT Plugins、Claude Tool Use、OpenAI Function Calling 的底层逻辑。

6. 输入历史(Arrow Key Navigation)

history.unshift(content) 记录,historyIndex 跟踪位置,inputDraft 保存未发送草稿。这是命令行 shell 历史的 Web 版实现。


10. 快速启动

环境要求

  • Python 3.11+
  • Node.js 18+

安装

# 克隆项目
git clone https://github.com/zhangyida-lab/bomx.git
cd bomx

# 安装 Python 依赖
pip install -e .

# 安装前端依赖
cd web && npm install && cd ..

# 配置环境变量
cp .env.example .env  # 编辑 .env,填入 LLM API Key

.env 配置

# 使用阿里千问(推荐)
PLM_LLM_API_KEY=sk-你的key
PLM_LLM_API_BASE=https://dashscope.aliyuncs.com/compatible-mode/v1
PLM_MODEL=openai/qwen-plus

# 或使用 Claude
# ANTHROPIC_API_KEY=sk-ant-...
# PLM_MODEL=claude-sonnet-4-6

# 后端地址(通常不用改)
PLM_API_URL=http://localhost:8001

启动服务

# Windows(推荐):同时启动后端 + 前端
start.bat

# 或手动启动:
# 终端1 - 后端
uvicorn app.main:app --reload --host 0.0.0.0 --port 8001

# 终端2 - 前端
cd web && npm run dev

初始化示例数据

python seed_examples.py

创建两个产品:智能健康手环(BAND-001)城市电动自行车(EB-001),各含 4 级 BOM。

访问

服务 地址
Web UI http://localhost:5173
API 文档 http://localhost:8001/docs
CLI Agent python plm.py

停止 / 重启

stop.bat      # 停止
restart.bat   # 重启

11. API 一览

零件

方法 路径 说明
GET /api/parts 列表(支持 q / part_type / status 过滤)
POST /api/parts 创建零件
GET /api/parts/{code} 详情(含版本列表)
PATCH /api/parts/{code} 更新
POST /api/parts/{code}/versions 新增版本
PATCH /api/parts/{code}/versions/{id}/activate 设为当前版本

BOM

方法 路径 说明
GET /api/bom BOM 列表
POST /api/bom 创建 BOM(传 part_version_id)
GET /api/bom/{id} BOM 详情(含明细行)
POST /api/bom/{id}/release 发布(发布后只读)
POST /api/bom/{id}/lines 添加 BOM 行
PATCH /api/bom/lines/{id} 修改 BOM 行
DELETE /api/bom/lines/{id} 删除 BOM 行

文档

方法 路径 说明
GET /api/documents 全局文档列表(含未关联文档)
POST /api/documents 上传独立文档
POST /api/bom/lines/{id}/documents 上传并关联到 BOM 行
POST /api/bom/lines/{id}/documents/attach 关联已有文档到 BOM 行
GET /api/bom/lines/{id}/documents/{link_id}/file 下载/预览文件
DELETE /api/bom/lines/{id}/documents/{link_id} 移除关联

AI 聊天

方法 路径 说明
GET /api/chat/config 返回当前模型名
POST /api/chat 发送消息(SSE 流式响应)

CLI Agent 工具清单

工具 说明
search_part(query) 模糊搜索零件
get_bom(part_code) 获取多级 BOM 树(终端渲染 + 返回文字摘要)
create_part(code, name, part_type) 创建零件
update_part(code, ...) 更新零件信息
add_bom_line(parent_code, child_code, quantity) 添加 BOM 行(自动建草稿)
remove_bom_line(line_id) 删除 BOM 行

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bomx-0.0.1.tar.gz (105.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bomx-0.0.1-py3-none-any.whl (38.2 kB view details)

Uploaded Python 3

File details

Details for the file bomx-0.0.1.tar.gz.

File metadata

  • Download URL: bomx-0.0.1.tar.gz
  • Upload date:
  • Size: 105.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for bomx-0.0.1.tar.gz
Algorithm Hash digest
SHA256 ac347829286211b3bcb5b556d313a51074d00e03e399fc8a0a9e47ec4c7fdcd5
MD5 d9f43906a633fb2609d838b6765f38ac
BLAKE2b-256 f241fd76b008e05461385a472a4a3436a2f54507d080e2ef8e04744e6177178c

See more details on using hashes here.

File details

Details for the file bomx-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: bomx-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 38.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.0

File hashes

Hashes for bomx-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b841cbd6c6f325aee5c17db5551af76220ad02e84ae5d51dc2bd4159451a6aae
MD5 842414aba776da62713836eb89b0a42f
BLAKE2b-256 23be2caae2b26cf30605cb4beb5df5eebcb539fbda15da0a7ecf2a0adcc6d052

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page