在这里插入图片描述

前言:一张发票引发的思考

上个月,我帮家里老人处理医疗报销。那是一个周五的下午,窗外下着小雨,我坐在电脑前,面前摊着一沓厚厚的医疗票据,开始一张张手动录入信息。

医院名称、就诊日期、药品明细、金额……每张发票都要重复这些机械的操作。两个小时过去了,我只处理了不到二十张。看着桌上还剩下的一大堆票据,我揉了揉发酸的眼睛,忍不住想:为什么这种重复性劳动还要人来做?

我是个程序员,平时天天和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张出错,那该多麻烦。

我开始研究如何提升识别率。经过大量测试,我发现了几个关键点:

  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
    
  2. 模型选择要匹配场景

    • 通用模型:适合多种票据类型,但准确率一般
    • 专用模型:针对医疗票据优化,准确率更高

最终,我选择了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不是智能体。

真正的智能体应该:

  1. 能理解:明白用户想要什么
  2. 会行动:知道该调用什么工具
  3. 能总结:用自然语言告诉用户结果

我设计的智能体工作流是这样的:

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?

  1. 复用性:封装成Skill后,任何人都可以通过魔搭平台使用
  2. 标准化:遵循统一的规范,便于集成到各种Agent框架
  3. 可测试:有明确的输入输出,便于验证和测试

封装后的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 什么是优秀的智能体?

经过这次实践,我对智能体有了更深的理解。

一个优秀的智能体需要:

  1. 强大的大脑

    • 能理解复杂的用户意图
    • 能总结工具执行结果
    • 能进行多轮对话
  2. 敏捷的双手

    • 能快速执行具体任务
    • 能处理各种边界情况
    • 能提供友好的错误提示
  3. 稳定的身体

    • 能在各种环境下稳定运行
    • 能处理大量并发请求
    • 能自动恢复错误

为什么端侧智能体很重要?

  1. 隐私保护
    医疗数据非常敏感,不能上传到云端。本地处理可以确保数据不离开设备。

  2. 低延迟
    本地处理可以实现毫秒级响应,用户体验更好。

  3. 离线可用
    即使没有网络也能正常工作,不受网络环境影响。

6.2 Hybrid AI的价值

在这次项目中,我深刻体会到Hybrid AI的价值。

什么是Hybrid AI?

Hybrid AI是指将大模型和小模型结合使用:

  • 大模型负责理解和总结(大脑)
  • 小模型负责具体任务(双手)

为什么需要Hybrid AI?

  1. 成本考虑
    大模型运行成本高,不适合频繁调用。小模型运行成本低,适合处理具体任务。

  2. 性能考虑
    大模型推理速度慢,小模型推理速度快。将两者结合,可以实现最佳性能。

  3. 能力考虑
    大模型能力强,小模型能力弱。将两者结合,可以实现最佳能力。

Hybrid AI的架构:

用户请求
    ↓
大模型(理解意图)
    ↓
判断是否需要调用工具
    ↓
小模型(执行任务)
    ↓
大模型(总结结果)
    ↓
返回给用户

6.3 Agentic Workflows的设计

在这次项目中,我设计了一个完整的Agentic Workflow。

Workflow的四个阶段:

  1. 理解阶段

    • 大模型理解用户意图
    • 提取关键信息
    • 判断是否需要调用工具
  2. 规划阶段

    • 确定需要调用哪些工具
    • 确定工具的调用顺序
    • 确定工具的参数
  3. 执行阶段

    • 调用工具执行任务
    • 处理工具执行结果
    • 处理错误情况
  4. 总结阶段

    • 大模型总结执行结果
    • 用自然语言回答用户
    • 提供后续建议

Workflow的优势:

  1. 可扩展性
    可以轻松添加新的工具和功能。

  2. 可维护性
    每个阶段的职责清晰,便于维护。

  3. 可测试性
    每个阶段都可以独立测试。


第七章:给其他开发者的建议

7.1 从小问题开始

不要一开始就追求大而全,先解决一个具体的小问题。

我一开始想做"医疗票据全流程处理",但后来发现这个范围太大了。于是我缩小范围,先做"医疗票据OCR识别",然后再逐步添加其他功能。

建议:

  1. 找一个真实的问题
  2. 定义清晰的边界
  3. 逐步迭代完善

7.2 选择合适的工具

OpenVINO真的很强大,可以充分释放Intel AI PC的潜力。

建议:

  1. 先了解各种工具的特点
  2. 根据场景选择合适的工具
  3. 不要盲目追求最新技术

7.3 重视稳定性

一个稳定运行的系统比一个功能强大但经常崩溃的系统更有价值。

建议:

  1. 做充分的错误处理
  2. 做大量的测试
  3. 监控系统的运行状态

7.4 多做测试

不同场景、不同设备都要测试,确保Skill的鲁棒性。

建议:

  1. 测试各种边界情况
  2. 测试各种设备环境
  3. 收集真实用户反馈

总结:一次难忘的技术之旅

这次比赛让我收获了很多:

技术上:

  • 学会了OpenVINO优化
  • 理解了Agentic AI架构
  • 掌握了端侧AI开发

认知上:

  • 理解了端侧AI的真正价值
  • 认识到稳定性的重要性
  • 体会到用户体验的关键

心态上:

  • 明白了做技术不仅仅是写代码,更是解决实际问题
  • 学会了从小问题开始,逐步迭代
  • 体会到了真实用户反馈的价值

从一张发票引发的思考,到构建出一个真正能干活的智能体,这段旅程让我深刻体会到:技术的价值在于解决实际问题。

现在,我的智能体已经能处理医疗票据了。但这只是开始,我还想做更多:

  1. 支持更多票据类型
    处方笺、病历、检查报告等

  2. 多工具协同
    OCR + RAG + 数据可视化,构建完整的医疗数据处理流程

  3. 端云协同
    本地处理敏感数据,云端获取最新的医保政策知识

我相信,未来的智能体将不仅仅是一个工具调用者,而是一个真正的"数字助手"。

最后,我想说:Agentic AI的时代已经到来。让我们一起,给智能体装上双手,让它真正地为我们工作。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐