AI-native PLM CLI agent for managing parts, BOMs, and documents via natural language
Project description
BOMX — AI-native PLM Agent
用自然语言管理零件和 BOM 的工程工具。
本文档面向学习者,重点解释"为什么这样设计"和"代码里值得学的地方"。
目录
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 → PartVersion,Document → 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file bomx-0.0.2.tar.gz.
File metadata
- Download URL: bomx-0.0.2.tar.gz
- Upload date:
- Size: 104.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27d015b5b84d9067a202b4dfc6bdc60cda46452e5fb8396c2b3c038825403eb2
|
|
| MD5 |
44c9b8b340efe2ce7a8c7465f3baca8f
|
|
| BLAKE2b-256 |
c32a2de087b93229058b1b75dda5ca84fe4a0b8dbd6870abefbc823dff6af3eb
|
File details
Details for the file bomx-0.0.2-py3-none-any.whl.
File metadata
- Download URL: bomx-0.0.2-py3-none-any.whl
- Upload date:
- Size: 19.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bba74e7cba8ff53288db839aabe7376b9c822af0a99f9db3b9c4357f57b41809
|
|
| MD5 |
cbe46a527dc7f8b2477808efcb13499a
|
|
| BLAKE2b-256 |
81ea8556b92d479a43144317c1b1bda6031842fd2e750a2c3d530f88c7d63006
|