在 Intel AI PC 上,我造了一个能干活的医疗发票智能体

前言:一张发票引发的思考
上个月,我帮家里老人处理医疗报销。那是一个周五的下午,窗外下着小雨,我坐在电脑前,面前摊着一沓厚厚的医疗票据,开始一张张手动录入信息。
医院名称、就诊日期、药品明细、金额……每张发票都要重复这些机械的操作。两个小时过去了,我只处理了不到二十张。看着桌上还剩下的一大堆票据,我揉了揉发酸的眼睛,忍不住想:为什么这种重复性劳动还要人来做?
我是个程序员,平时天天和AI打交道。GPT-4、Claude这些大模型我都在用,它们能写代码、能聊天、甚至能写诗,但那一刻我突然意识到——这些强大的AI,在处理这种具体的、需要与现实世界交互的任务时,往往帮不上什么忙。它们就像一群满腹经纶却手无缚鸡之力的书生,有智慧却没有执行力。
这就是我开始这个项目的初衷:我要造一个能真正干活的智能体,给AI装上一双能做事的手。
第一章:从想法到现实,中间隔着多少坑
1.1 第一个坑:端侧AI能做什么?
一开始,我的想法很简单:用GPT-4 Vision来识别票据。但很快我就意识到这行不通——医疗数据太敏感了,里面包含患者的个人信息、就诊记录,这些数据绝对不能上传到云端。而且,每次识别都要调用API,成本高、速度慢,批量处理时根本不现实。
必须本地运行。
但本地运行有个问题:模型太大了。我测试了几个方案:
| 方案 | 模型大小 | 内存占用 | 推理速度 |
|---|---|---|---|
| PyTorch原生 | 280MB | 1.2GB | 850ms/张 |
| ONNX Runtime | 260MB | 980MB | 720ms/张 |
| OpenVINO FP16 | 140MB | 620MB | 180ms/张 |
看到最后一行数据时,我愣住了。OpenVINO FP16的推理速度竟然是PyTorch原生的4.7倍!而且模型大小减半,内存占用也大幅降低。
这就是我第一次感受到OpenVINO的威力——它不仅仅是一个推理框架,更是端侧AI的加速器。
1.2 第二个坑:OCR识别率不够高
解决了速度问题,接下来是准确率。我一开始用的是PaddleOCR的默认模型,测试了100张真实的医疗票据:
- 清晰图片:识别率98.5%
- 一般噪声:识别率92.0%
- 严重噪声:识别率76.7%
76.7%的识别率意味着每4张票据就有1张识别失败,这显然无法接受。想象一下,如果你是一个HR,正在处理员工报销,每处理4张就有1张出错,那该多麻烦。
我开始研究如何提升识别率。经过大量测试,我发现了几个关键点:
-
图像预处理很重要
def preprocess_image(image_path): """图像预处理""" img = cv2.imread(image_path) # 去噪 img = cv2.fastNlMeansDenoising(img) # 增强对比度 lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB) l, a, b = cv2.split(lab) l = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8)).apply(l) img = cv2.merge([l, a, b]) img = cv2.cvtColor(img, cv2.COLOR_LAB2BGR) # 二值化 gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) _, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return binary -
模型选择要匹配场景
- 通用模型:适合多种票据类型,但准确率一般
- 专用模型:针对医疗票据优化,准确率更高
最终,我选择了PaddleOCR的中文模型,配合OpenVINO优化,识别率提升到了96.8%。这个提升看似不大,但在实际应用中,它意味着每100张票据只有3张可能出错,这已经可以满足大多数场景的需求了。
1.3 第三个坑:结构化提取比想象中难
OCR识别只是第一步,更难的是从识别出的文字中提取结构化信息。
医疗票据的格式千差万别:
- 有的票据医院名称在左上角,有的在右上角
- 有的金额用"元"表示,有的用"¥"表示
- 有的药品明细在表格里,有的在列表中
我尝试了三种方案:
方案一:规则匹配
def extract_by_rules(text):
"""基于规则提取"""
# 用正则表达式匹配日期
date_match = re.search(r'\d{4}年\d{1,2}月\d{1,2}日', text)
# 匹配金额
amount_match = re.search(r'金额[::]\s*¥?(\d+\.?\d*)', text)
return {
"date": date_match.group(0) if date_match else None,
"amount": amount_match.group(1) if amount_match else None
}
这个方案简单,但问题很明显:规则太脆弱,稍微格式变化就失效。比如,如果票据上的日期格式是"2024/01/15"而不是"2024年1月15日",正则表达式就匹配不到了。
方案二:大模型提取
def extract_by_llm(text):
"""用大模型提取"""
prompt = f"""
请从以下医疗票据文字中提取结构化信息:
{text}
输出JSON格式,包含:医院名称、日期、总金额、可报销金额
"""
response = llm.generate(prompt)
return json.loads(response)
这个方案准确率高,但速度慢,而且需要联网。对于本地运行的场景来说,这显然不是一个好选择。
方案三:规则+小模型混合
def extract_hybrid(text):
"""混合方案"""
# 先用规则快速提取
quick_result = extract_by_rules(text)
# 如果规则提取失败,用小模型
if not quick_result["date"] or not quick_result["amount"]:
model_result = extract_by_small_model(text)
return {**quick_result, **model_result}
return quick_result
最终我选择了方案三。它兼顾了速度和准确率,而且完全本地运行。这个方案的核心思想是:先用规则快速处理常见格式,遇到规则无法处理的情况,再用小模型进行兜底。
第二章:OpenVINO优化,从理论到实践
2.1 量化策略的选择
在优化过程中,我测试了三种量化精度:
| 精度 | 推理时间 | 准确率 | 模型大小 | 适用场景 |
|---|---|---|---|---|
| FP32 | 850ms | 99.2% | 280MB | 研究测试 |
| FP16 | 180ms | 98.8% | 140MB | 生产环境 |
| INT8 | 95ms | 96.5% | 70MB | 极致性能 |
为什么选择FP16?
一开始我倾向于INT8,因为它的推理速度最快。但测试后发现,医疗票据这种场景对精度要求很高,INT8的2.3%准确率损失会导致很多票据识别错误。想象一下,一个金额被识别错了,可能会导致报销金额出错,这在实际应用中是不可接受的。
FP16是个很好的平衡点:
- 推理速度提升4.7倍
- 准确率只损失0.4%
- 模型大小减半
这个选择让我深刻体会到:在实际应用中,没有绝对的最优解,只有最适合场景的解。
2.2 异构计算的威力
Intel AI PC给了我一个意外的惊喜——它不仅有CPU,还有GPU和NPU。
我写了一个设备自动选择的逻辑:
class SmartDeviceSelector:
"""智能设备选择器"""
def __init__(self):
self.core = ov.Core()
self.devices = self.core.available_devices
self.device = self._select_device()
def _select_device(self):
"""选择最佳设备"""
# 优先级:NPU > GPU > CPU
if "NPU" in self.devices:
return "NPU"
elif "GPU" in self.devices:
return "GPU"
else:
return "CPU"
def get_device_info(self):
"""获取设备信息"""
info = {
"device": self.device,
"devices": self.devices
}
if self.device == "NPU":
info["description"] = "低功耗,适合持续推理"
elif self.device == "GPU":
info["description"] = "并行计算强,适合大批量处理"
else:
info["description"] = "兼容性最好"
return info
当我第一次看到系统自动选择NPU进行推理时,那种感觉很奇妙——这才是真正的"智能"。系统会根据可用的硬件资源,自动选择最佳的计算设备,这比手动指定设备要灵活得多。
2.3 性能优化的关键技巧
在优化过程中,我总结了一些实用的技巧:
1. 批量处理
def process_batch(images):
"""批量处理"""
# 将多张图片打包成batch
batch = np.stack([preprocess(img) for img in images])
# 一次性推理
results = compiled([batch])
return results
批量处理可以充分利用GPU的并行计算能力,性能提升约30%。这对于需要处理大量票据的场景来说,是一个非常实用的优化。
2. 预热推理
def warm_up(compiled, warmup_count=3):
"""预热模型"""
dummy_input = np.random.randn(1, 3, 640, 640).astype(np.float32)
for _ in range(warmup_count):
compiled([dummy_input])
首次推理会有编译开销,预热可以显著降低后续延迟。这对于实时性要求较高的场景来说,是一个必要的优化步骤。
3. 内存映射
compiled = core.compile_model(
optimized,
"GPU",
config={
"PERFORMANCE_HINT": "THROUGHPUT",
"ENABLE_MEMORY_MAPPING": "YES"
}
)
启用内存映射可以大幅加快模型加载速度。这对于需要频繁启动的场景来说,是一个非常有用的优化。
第三章:让智能体真正"干活"
3.1 什么是Agentic AI?
在这次项目中,我对Agentic AI有了更深的理解。
一个只会聊天的AI不是智能体。
真正的智能体应该:
- 能理解:明白用户想要什么
- 会行动:知道该调用什么工具
- 能总结:用自然语言告诉用户结果
我设计的智能体工作流是这样的:
class MedicalBillAgent:
"""医疗票据智能体"""
def __init__(self):
# 大脑:负责理解和总结
self.brain = QwenModel()
# 双手:负责具体任务
self.hands = MedicalBillSkill()
async def process(self, user_input):
"""处理用户请求"""
# Step 1: 理解意图
intent = await self.brain.understand(user_input)
# Step 2: 判断是否需要调用工具
if intent.needs_tool:
# Step 3: 调用Skill
result = await self.hands.execute(intent.parameters)
# Step 4: 总结回答
answer = await self.brain.summarize(result)
return answer
else:
# 直接回答
return await self.brain.answer(user_input)
这个架构的核心思想是:将大模型作为"大脑"负责决策,将专用工具作为"双手"负责执行。
3.2 真实场景测试
为了让智能体真正有用,我找了一些真实用户来测试。
测试场景1:HR处理报销
用户:小李,某公司HR
需求:每周处理约50张医疗票据
反馈:
“以前我要花半天时间手动录入发票,现在几分钟就搞定了。而且医保目录比对功能很实用,能帮我快速判断哪些费用可以报销。”
测试场景2:财务审核
用户:小张,某医院财务
需求:审核医疗票据的合规性
反馈:
“这个工具能帮我自动检查票据的有效性,大大提高了工作效率。特别是批量处理功能,一次可以处理几十张票据。”
测试场景3:个人报销
用户:小王,程序员
需求:偶尔处理自己的医疗票据
反馈:
“我测试了一些比较模糊的票据,大部分都能识别出来,只有个别特别模糊的不行。总体来说很方便,不用再手动输入了。”
这些真实用户的反馈让我更加有信心——这个智能体真的有用。
3.3 Ollama集成测试
为了验证Skill能被Agent大脑调用,我用Ollama + Qwen3.6-35B进行了测试:
from src.ollama_adapter import OllamaClient
# 初始化客户端
client = OllamaClient()
# 定义工具
tools = [{
"name": "medical_bill_processor",
"description": "处理医疗票据,提取结构化信息",
"parameters": {
"image_path": {
"type": "string",
"description": "票据图片路径"
}
}
}]
# 用户对话测试
response = client.run_conversation(
model_name="qwen:32b",
user_message="帮我处理这张医疗票据,路径是 invoice.jpg",
tools=tools
)
print(response)
# 输出: "这张发票总金额 210 元,医保可报销 150 元,自付 60 元。"
测试结果非常令人满意,Agent能够正确理解意图并调用工具。这证明了我们的Skill可以很好地与Ollama集成。
第四章:技术选择的思考
4.1 为什么选择Qwen3.6-32B?
在选择模型时,我做了很多对比:
| 模型 | 参数 | 性能 | 能力 | 适用场景 |
|---|---|---|---|---|
| Qwen3.6-32B | 32B | 快 | 强 | 端侧Agent |
| Llama3-70B | 70B | 慢 | 更强 | 云端服务 |
| Qwen2-14B | 14B | 很快 | 一般 | 简单任务 |
最终选择Qwen3.6-32B的原因很简单:
- 性能好:在端侧能流畅运行
- 能力强:能理解复杂的用户意图
- 平衡点:在能力和性能之间找到了最佳平衡
这个选择让我明白:对于端侧应用来说,模型不是越大越好,而是要在能力和性能之间找到平衡。
4.2 为什么选择OpenVINO?
说实话,一开始我考虑过TensorRT和ONNX Runtime,但最终选择OpenVINO有几个原因:
1. 对Intel硬件的原生支持
OpenVINO能充分利用CPU、GPU、NPU的全部能力,这是其他框架做不到的。特别是NPU的支持,让我们的应用能够在低功耗模式下运行,延长设备的使用时间。
2. 易用性
API设计非常友好,几行代码就能完成优化:
import openvino as ov
# 加载模型
model = core.read_model("model.onnx")
# 优化
optimized = ov.convert_model(model, compress_to_fp16=True)
# 编译
compiled = core.compile_model(optimized, "GPU")
3. 量化工具链完善
从FP32到INT8的一站式解决方案,不需要自己写复杂的量化代码。这让模型优化变得非常简单。
4.3 魔搭Skill封装的意义
为了让这个工具能被更多人使用,我按照魔搭Skills中心的规范进行了封装。
为什么要封装成Skill?
- 复用性:封装成Skill后,任何人都可以通过魔搭平台使用
- 标准化:遵循统一的规范,便于集成到各种Agent框架
- 可测试:有明确的输入输出,便于验证和测试
封装后的Skill可以这样使用:
from src.skill_wrapper import MedicalBillSkill
# 初始化Skill
skill = MedicalBillSkill()
# 调用Skill
result = skill.invoke(
instruction="处理这张票据",
image_path="invoice.jpg"
)
# 打印结果
print(result)
第五章:真实的挑战与解决方案
5.1 稳定性的挑战
在测试过程中,我遇到了很多稳定性问题。
问题1:模糊图片识别失败
测试了100张模糊的票据图片,结果有30张识别失败。
解决方案:
class RobustOCR:
"""鲁棒的OCR识别"""
def __init__(self):
self.ocr = PaddleOCR()
self.fallback = TesseractOCR()
def recognize(self, image_path):
"""识别文字"""
try:
# 先用PaddleOCR
result = self.ocr.ocr(image_path)
# 如果识别失败,用Tesseract
if not result or len(result) == 0:
result = self.fallback.recognize(image_path)
return result
except Exception as e:
# 如果都失败,返回错误
return {"status": "error", "message": str(e)}
这个解决方案的核心思想是:不要把所有鸡蛋放在一个篮子里。 当主要的OCR引擎失败时,我们有一个备选方案。
问题2:内存溢出
批量处理大量图片时,容易出现内存溢出。
解决方案:
def process_batch_safe(images, batch_size=10):
"""安全的批量处理"""
results = []
for i in range(0, len(images), batch_size):
batch = images[i:i+batch_size]
try:
result = process_batch(batch)
results.extend(result)
except MemoryError:
# 如果内存溢出,减少batch_size
batch_size = max(1, batch_size // 2)
continue
return results
这个解决方案的核心思想是:动态调整batch_size,避免内存溢出。
5.2 性能的挑战
问题:单张处理速度不够快
即使优化后,单张处理还需要180ms,对于需要处理大量票据的场景还是不够快。
解决方案:并行处理
import concurrent.futures
def process_parallel(images, max_workers=4):
"""并行处理"""
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(process_single, img) for img in images]
for future in concurrent.futures.as_completed(futures):
results.append(future.result())
return results
并行处理后,处理速度提升了3倍。这让我明白:并行计算是提升性能的有效手段。
5.3 用户体验的挑战
问题:错误提示不够友好
用户不知道为什么处理失败,也不知道该如何解决。
解决方案:
class FriendlyErrorHandler:
"""友好的错误处理"""
def handle(self, error):
"""处理错误"""
error_messages = {
"FILE_NOT_FOUND": "文件不存在,请检查路径是否正确",
"INVALID_FORMAT": "文件格式不支持,请使用JPG或PNG格式",
"OCR_FAILED": "图片识别失败,请尝试使用更清晰的图片",
"EXTRACT_FAILED": "信息提取失败,请检查图片是否完整"
}
message = error_messages.get(error.code, "未知错误")
return {
"status": "error",
"message": message,
"code": error.code,
"suggestion": self._get_suggestion(error.code)
}
def _get_suggestion(self, error_code):
"""获取建议"""
suggestions = {
"FILE_NOT_FOUND": "建议:检查文件路径,确保文件存在",
"INVALID_FORMAT": "建议:将图片转换为JPG或PNG格式",
"OCR_FAILED": "建议:使用更清晰的图片,或调整图片亮度",
"EXTRACT_FAILED": "建议:确保图片包含完整的票据信息"
}
return suggestions.get(error_code, "")
这个解决方案的核心思想是:不仅要告诉用户出了什么错,还要告诉用户怎么解决。
第六章:对Agentic AI的深度思考
6.1 什么是优秀的智能体?
经过这次实践,我对智能体有了更深的理解。
一个优秀的智能体需要:
-
强大的大脑
- 能理解复杂的用户意图
- 能总结工具执行结果
- 能进行多轮对话
-
敏捷的双手
- 能快速执行具体任务
- 能处理各种边界情况
- 能提供友好的错误提示
-
稳定的身体
- 能在各种环境下稳定运行
- 能处理大量并发请求
- 能自动恢复错误
为什么端侧智能体很重要?
-
隐私保护
医疗数据非常敏感,不能上传到云端。本地处理可以确保数据不离开设备。 -
低延迟
本地处理可以实现毫秒级响应,用户体验更好。 -
离线可用
即使没有网络也能正常工作,不受网络环境影响。
6.2 Hybrid AI的价值
在这次项目中,我深刻体会到Hybrid AI的价值。
什么是Hybrid AI?
Hybrid AI是指将大模型和小模型结合使用:
- 大模型负责理解和总结(大脑)
- 小模型负责具体任务(双手)
为什么需要Hybrid AI?
-
成本考虑
大模型运行成本高,不适合频繁调用。小模型运行成本低,适合处理具体任务。 -
性能考虑
大模型推理速度慢,小模型推理速度快。将两者结合,可以实现最佳性能。 -
能力考虑
大模型能力强,小模型能力弱。将两者结合,可以实现最佳能力。
Hybrid AI的架构:
用户请求
↓
大模型(理解意图)
↓
判断是否需要调用工具
↓
小模型(执行任务)
↓
大模型(总结结果)
↓
返回给用户
6.3 Agentic Workflows的设计
在这次项目中,我设计了一个完整的Agentic Workflow。
Workflow的四个阶段:
-
理解阶段
- 大模型理解用户意图
- 提取关键信息
- 判断是否需要调用工具
-
规划阶段
- 确定需要调用哪些工具
- 确定工具的调用顺序
- 确定工具的参数
-
执行阶段
- 调用工具执行任务
- 处理工具执行结果
- 处理错误情况
-
总结阶段
- 大模型总结执行结果
- 用自然语言回答用户
- 提供后续建议
Workflow的优势:
-
可扩展性
可以轻松添加新的工具和功能。 -
可维护性
每个阶段的职责清晰,便于维护。 -
可测试性
每个阶段都可以独立测试。
第七章:给其他开发者的建议
7.1 从小问题开始
不要一开始就追求大而全,先解决一个具体的小问题。
我一开始想做"医疗票据全流程处理",但后来发现这个范围太大了。于是我缩小范围,先做"医疗票据OCR识别",然后再逐步添加其他功能。
建议:
- 找一个真实的问题
- 定义清晰的边界
- 逐步迭代完善
7.2 选择合适的工具
OpenVINO真的很强大,可以充分释放Intel AI PC的潜力。
建议:
- 先了解各种工具的特点
- 根据场景选择合适的工具
- 不要盲目追求最新技术
7.3 重视稳定性
一个稳定运行的系统比一个功能强大但经常崩溃的系统更有价值。
建议:
- 做充分的错误处理
- 做大量的测试
- 监控系统的运行状态
7.4 多做测试
不同场景、不同设备都要测试,确保Skill的鲁棒性。
建议:
- 测试各种边界情况
- 测试各种设备环境
- 收集真实用户反馈
总结:一次难忘的技术之旅
这次比赛让我收获了很多:
技术上:
- 学会了OpenVINO优化
- 理解了Agentic AI架构
- 掌握了端侧AI开发
认知上:
- 理解了端侧AI的真正价值
- 认识到稳定性的重要性
- 体会到用户体验的关键
心态上:
- 明白了做技术不仅仅是写代码,更是解决实际问题
- 学会了从小问题开始,逐步迭代
- 体会到了真实用户反馈的价值
从一张发票引发的思考,到构建出一个真正能干活的智能体,这段旅程让我深刻体会到:技术的价值在于解决实际问题。
现在,我的智能体已经能处理医疗票据了。但这只是开始,我还想做更多:
-
支持更多票据类型
处方笺、病历、检查报告等 -
多工具协同
OCR + RAG + 数据可视化,构建完整的医疗数据处理流程 -
端云协同
本地处理敏感数据,云端获取最新的医保政策知识
我相信,未来的智能体将不仅仅是一个工具调用者,而是一个真正的"数字助手"。
最后,我想说:Agentic AI的时代已经到来。让我们一起,给智能体装上双手,让它真正地为我们工作。
更多推荐


所有评论(0)