惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
云风的 BLOG
云风的 BLOG
大猫的无限游戏
大猫的无限游戏
M
MIT News - Artificial intelligence
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Recent Announcements
Recent Announcements
IT之家
IT之家
Google DeepMind News
Google DeepMind News
罗磊的独立博客
爱范儿
爱范儿
Last Week in AI
Last Week in AI
人人都是产品经理
人人都是产品经理
U
Unit 42
MongoDB | Blog
MongoDB | Blog
S
SegmentFault 最新的问题
B
Blog
博客园 - 叶小钗
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
C
Check Point Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

博客园_首页

Plist 二进制格式 Milvus 和 PGVector,哪个更好? OpenClaw 已过时?在 VS Code 中运行 Hermes Agent! 第30篇文章:一个大三计科生的自白 Manim如何在数学公式中完美显示中文? Docker 部署 RocketMQ 5 并发编程核心概念辨析 C#事务处理最佳实践:别再让“主表存了、明细丢了”的破事发生 CLI 是什么?为什么大厂突然集体卷命令行? 【从0到1构建一个ClaudeAgent】协作-自主Agent UIImageView 设置图片不生效的原因排查 最小二乘问题详解20:无先验约束下的增量式SFM自由网平差 痞子衡嵌入式:大话双核i.MXRT1180之XIP应用里借助MU实现可靠Flash IAP的方法 AI Chat 封装, SemanticKerne.AiProvider.Unified 已发布 Windows下右键编辑js文件无法打开记事本——在注册表中使用环境变量 在后台服务中使用 Scoped 服务,为什么总是报错? H200 安装驱动并使用sglang启动模型 wireshark 抓包Trap上报告警内容 我用 AI 辅助开发了一系列小工具(2):图片压缩工具 [A Primer On MC and CC] 2.1 Memory Consistency 1 - 指令重排序和 SC 模型 Oracle数据库SCN推进技术详解与实践指南 玩转控件:封装个带图片的Label控件 Claude Code 4.7 真正该升级的不是模型,而是你的工作流 前端小白一句话,AI 帮我做了个颜值拉满的桌面媒体播放器。当代码不再是门槛,一句话编程就是现实。 5. WorkBuddy: 小龙虾的灵魂三件套,让你的小龙虾不只是工具 SQLite 分片方案实战:三种分片策略的深度对比 告别简陋 UI!一款基于 Fluent Design 和基于 WinUI 的开源免费、现代化的 Avalonia UI 控件库 关于二进制排列组合枚举的总结 AI开发-python-LangGraph框架(3-27-LangGraph从零实现大模型智能决策工作流) ElasticSearch主分片和副本分片概念详解
张高兴的 Hailo-10 开发指南:(二)使用 LangChain 搭建本地...
张高兴 · 2026-06-07 · via 博客园_首页

上一篇博客介绍了在 Hailo-10 上实现语音识别应用,这次我们来聊聊它的另一个重要功能:本地大模型推理。当然如果只是做离线对话的话,应用场景可能比较有限,下面结合 LangChain 实现一个简单的 RAG 应用。设备仍然使用之前介绍的 ASUS UGen300。

1

环境配置

与音频模型只能在 Linux 上运行不同,本地大模型推理可以在 Windows 上实现。这里是在安装了 Raspberry Pi OS 的 Raspberry Pi 5 上实现的。

安装 HailoRT

首先要安装 HailoRT,这是 Hailo 的运行时库,包含设备驱动、Python 绑定、命令行工具。可以去 Hailo Developer Zone 注册账号下载,也可以在 ASUS 的官网下载。详细步骤见上一篇博客,张高兴的 Hailo-10 开发指南:(一)实现离线语音识别

安装 Hailo-Ollama

Hailo-Ollama 是 Hailo 官方提供的推理服务,API 和 Ollama 完全兼容,意味着能用 Ollama 的工具(LangChain、Open-WebUI 等)基本都能直接使用。在安装完 ASUS 的驱动捆绑包之后,Hailo-Ollama 也已经安装完成。如果你的 Hailo-10 是 M.2 版本的,可能需要单独安装 Hailo-Ollama,在 Hailo Developer Zone 中下载安装 Hailo Model Zoo GenAI

2

Python 环境

pip install pdfplumber langchain langchain-ollama langchain-text-splitters \
            langchain-chroma langchain-huggingface sentence-transformers \
            chromadb requests

启动 Hailo-Ollama 服务

服务启动很简单,直接在命令行中运行下面的命令,默认监听 0.0.0.0:8000

hailo-ollama

下面看看 Hailo-10 目前支持哪些模型:

curl --silent http://localhost:8000/hailo/v1/list

可以看到目前支持 deepseek_r1:1.5bllama3.2:1bqwen2.5-coder:1.5bqwen2.5:1.5bqwen2:1.5bqwen3:1.7b 这些模型。执行下面的命令拉取 qwen3:1.7b 模型:

curl --silent http://localhost:8000/api/pull \
     -H 'Content-Type: application/json' \
     -d '{ "model": "qwen3:1.7b", "stream" : true }'

拉取完成后,验证一下模型是否能正常推理:

curl --silent http://localhost:8000/api/chat \
     -H 'Content-Type: application/json' \
     -d '{"model": "qwen3:1.7b", "messages": [{"role": "user", "content": "用一句话解释什么是向量数据库"}]}'

到这里,整个链路就通了。接下来你可以通过调用接口的方式在自己的应用里使用这个模型了,也可以使用支持 Ollama 的工具,比如 Open-WebUI 来和模型进行交互。比如拉取 Open-WebUI 的 Docker 镜像:

docker run -d --net=host -e OLLAMA_BASE_URL=http://127.0.0.1:8000 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main

3

实现 RAG 应用

模型本身不会主动知道你公司内部文档里写了什么。RAG 是把文档切碎存进向量数据库,提问时先搜索最相关的段落,再把这些段落塞进 prompt 让模型回答。通过 RAG 模型可以理解一些最新的、专业的知识,而不需要模型本身学会这些知识。比如这里使用的是 ASUS UGen300 的用户手册,模型本身不可能学会里面的内容,但通过 RAG 的方式,模型就能理解用户手册里的内容了。当然你也可以替换成其他的文档完成后面的操作。

4

核心流程很简单:文档处理 → 向量化 → RAG 链路 → 问答。下面新建 rag_demo.py 文件.

1. 引用相关包

import json
import os
import requests
from typing import Any, Iterator, List, Optional
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import (
    AIMessage,
    AIMessageChunk,
    BaseMessage,
    HumanMessage,
    SystemMessage,
)
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
from langchain_core.documents import Document
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
import pdfplumber

2. PDF 文档处理

pdfplumber 逐页解析 PDF,构造 LangChain Document 对象。

PDF_FILES = [
    "data/UGen300-manual.pdf",
]
all_docs = []

for pdf_path in PDF_FILES:
     if not os.path.exists(pdf_path):
          print(f"  跳过(不存在):{pdf_path}")
          continue
     count = 0
     with pdfplumber.open(pdf_path) as pdf:
          for i, page in enumerate(pdf.pages):
               text = (page.extract_text() or "").strip()
               all_docs.append(Document(
                    page_content=text,
                    metadata={"source": pdf_path, "page": i + 1},
               ))
               count += 1

print(f"共加载 {len(all_docs)} 页文本")

3. 文本切分

文档切分成更小的段落,方便后续的向量化和检索。chunk_size 为每个 chunk 最大字符数,chunk_overlap 让相邻 chunk 共享边界内容,避免关键信息被截断。

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)
chunks = splitter.split_documents(all_docs)
print(f"共生成 {len(chunks)} 个 chunk")

4. 向量化和存储

使用 HuggingFaceEmbeddings 生成文本的向量表示,并存储到 ChromaDB。all-MiniLM-L6-v2 是最常用的轻量通用 embedding 模型,速度快,在本地运行,不需要 API key,也不依赖 GPU。ChromaDB 是一个轻量的本地向量数据库,适合小规模数据的存储和检索。

CHROMA_DB_DIR    = "./chroma_db"
COLLECTION_NAME  = "hailo_docs"

embeddings = HuggingFaceEmbeddings(
    model_name="sentence-transformers/all-MiniLM-L6-v2",
    model_kwargs={"device": "cpu"},
    encode_kwargs={"normalize_embeddings": True}
)
print("Embedding 模型就绪")

db_exists = os.path.exists(CHROMA_DB_DIR) and os.listdir(CHROMA_DB_DIR)

if db_exists:
    # 数据库已存在,直接加载
    vectorstore = Chroma(
        collection_name=COLLECTION_NAME,
        embedding_function=embeddings,
        persist_directory=CHROMA_DB_DIR,
    )
    print(f"已加载现有数据库 {CHROMA_DB_DIR}")
else:
    # 首次构建
    vectorstore = Chroma.from_documents(
        documents=chunks,
        embedding=embeddings,
        collection_name=COLLECTION_NAME,
        persist_directory=CHROMA_DB_DIR,
    )
    print(f"数据库构建完成,已持久化到 {CHROMA_DB_DIR}")

5. 自定义 HailoChatOllama 类

Hailo-Ollama 的底层用的是 oatpp HTTP 框架,它对请求体的 JSON 字段类型要求很严。LangChain 官方的 ChatOllama 会往请求里附加 optionskeep_alive 等额外字段,oatpp 碰到未知字段直接报类型解析错误。

解决方案是继承 BaseChatModel 自己写一个简化版:

class HailoChatOllama(BaseChatModel):
    model: str
    base_url: str
    timeout: int = 120

    @property
    def _llm_type(self) -> str:
        return "hailo-ollama"

    def _to_ollama_messages(self, messages: List[BaseMessage]) -> list:
        role_map = {"system": "system", "human": "user", "ai": "assistant"}
        return [
            {"role": role_map.get(m.type, m.type), "content": m.content}
            for m in messages
        ]

    def _generate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Any = None,
        **kwargs: Any,
    ) -> ChatResult:
        payload = {
            "model": self.model,
            "messages": self._to_ollama_messages(messages),
            "stream": False,
        }
        resp = requests.post(
            f"{self.base_url}/api/chat",
            json=payload,
            timeout=self.timeout,
        )
        resp.raise_for_status()
        content = resp.json()["message"]["content"]
        return ChatResult(
            generations=[ChatGeneration(message=AIMessage(content=content))]
        )

    def _stream(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Any = None,
        **kwargs: Any,
    ) -> Iterator[ChatGenerationChunk]:
        payload = {
            "model": self.model,
            "messages": self._to_ollama_messages(messages),
            "stream": True,
        }
        with requests.post(
            f"{self.base_url}/api/chat",
            json=payload,
            stream=True,
            timeout=self.timeout,
        ) as resp:
            resp.raise_for_status()
            for line in resp.iter_lines():
                if not line:
                    continue
                data = json.loads(line.decode("utf-8"))
                token = data.get("message", {}).get("content", "")
                if token:
                    chunk = ChatGenerationChunk(
                        message=AIMessageChunk(content=token)
                    )
                    if run_manager:
                        run_manager.on_llm_new_token(token, chunk=chunk)
                    yield chunk
                if data.get("done"):
                    break

只传 modelmessages 两个字段,完全避开 oatpp 的字段校验问题。

6. RAG 链的组装

这里搭建了一个最基本的 RAG 链路,根据用户提问先用检索器从 ChromaDB 取最相关的 3 个 chunk,把它们拼成一个上下文,再把上下文和问题一起塞进 prompt 让模型回答。

# 创建检索器,相似度搜索,返回 top_k 最相关 chunk
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

llm = HailoChatOllama(
    model="qwen3:1.7b",
    base_url="http://localhost:8000"
)

# 问答函数
def stream_answer(question: str):
    docs = retriever.invoke(question)
    context = "\n\n---\n\n".join(d.page_content for d in docs)
    prompt = f"""
你是一个专业助手,只根据下面提供的参考文档尽可能简短的回答问题。如果文档中没有相关信息,请直接说"文档中未找到相关内容"。

文档内容:
{context}

问题:{question}
"""
    
    for chunk in llm.stream([HumanMessage(content=prompt)]):
        print(chunk.content, end="", flush=True)
    print()

stream_answer("How to install HailoRT on Ubuntu?")

5

LoRA 微调

除了 RAG 可以使模型获取外部知识之外,LoRA 微调可以让模型学会一些特定的技能。比如你想让模型更懂你公司的业务术语,或者学会一些特定的操作流程,这时候就可以通过 LoRA 微调来实现。LoRA 微调的原理是只训练模型中一小部分参数(通常是权重矩阵的低秩分解),而保持其他参数冻结不变。这样可以大幅降低微调所需的计算资源和时间,同时还能让模型学会新的技能。

我一开始的设想是类似之前做视觉模型一样(将自定义模型编译为 Hailo NPU 的 .hef 模型),先任选一个通用基座进行微调,然后导出 ONNX 模型,最后将 ONNX 模型编译成 HEF 模型在 Hailo-10 上部署。结果发现目前不支持用户自定义模型,只能使用官方提供的模型。这也是目前 Hailo-10 在大模型推理方面的一个限制。

不过 Hailo 官方在 Hailo Dataflow Compiler 用户手册的第 4.8 章和第 6 章提供了一种特殊的微调方案,仅支持 Qwen2-1.5B-Instruct 这个模型。需要将微调后的 LoRA adapter 权重挂载到预优化的 HAR 文件中,然后量化编译为 HEF 模型。这个过程需要的硬件配置较高,需要一块 32GB 显存的 GPU 和 128GB 的内存。下面根据官方文档简单的介绍一下这个流程,这里我并没有进行测试。

1. 微调

在微调时,Hailo DFC 有一些硬性约束,不满足就编译报错。这意味着不能用常见的 r=8r=16 配置,秩必须是 32。这个约束来自 Hailo 硬件对 LoRA 矩阵乘法的专用实现。

参数 要求
lora_alpha 必须等于 64
r(秩) 必须等于 32
target_modules 只能是 FFN 的 gate_projdown_projup_proj
import torch
from datasets import load_dataset
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig
from trl import SFTConfig, SFTTrainer

# 1. 基础配置
MODEL_ID = "Qwen/Qwen2-1.5B-Instruct"
OUTPUT_DIR = "./lora_adapter_news"

# 2. 加载模型与分词器,关闭 KV cache 节省显存
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, torch_dtype="auto", device_map="auto")
model.config.use_cache = False
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)

# 3. 准备数据集 (AG News)
dataset = load_dataset("fancyzhx/ag_news", split="test[:1000]") # 取前1000条快速演示

def format_data(example):
    # 转换为 Qwen 对话格式
    messages = [
        {"role": "system", "content": "Classify news into: world, sports, business, sci/tech."},
        {"role": "user", "content": example["text"]}
    ]
    prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    labels = ["world", "sports", "business", "sci/tech"]
    return {"text": prompt + labels[example["label"]]}

formatted_dataset = dataset.map(format_data)

# 4. Hailo 兼容的 LoRA 配置
peft_config = LoraConfig(
    lora_alpha=64,            # 必须为 64
    r=32,                     # 必须为 32
    target_modules=["gate_proj", "down_proj", "up_proj"], # 仅支持 FFN 层
    task_type="CAUSAL_LM",
)

# 5. 开始训练
training_args = SFTConfig(
    dataset_text_field="text",
    max_seq_length=512,
    output_dir=OUTPUT_DIR,
    num_train_epochs=1,
    per_device_train_batch_size=4,
)

trainer = SFTTrainer(
    model=model,
    train_dataset=formatted_dataset,
    peft_config=peft_config,
    args=training_args,
)

trainer.train()
print(f"LoRA 权重已保存至: {OUTPUT_DIR}")

2. 编译 HEF 模型

将基础模型和 LoRA 权重融合,量化并编译为专用的二进制 .hef 文件。需要提前从 Hailo 官方获取 Qwen2-1.5B 的预优化文件(放在 ./models 目录下):

import os
import tensorflow as tf
from hailo_sdk_client.runner.client_runner import ClientRunner

MODEL_NAME = "qwen2_1.5b_instruct"
ADAPTER_NAME = "lora_adapter_news"

# 1. 初始化 Runner 并加载基础 HAR
runner = ClientRunner(hw_arch="hailo10h")
runner.load_har(f"./models/{MODEL_NAME}.q.har")

# 2. 挂载阶段一训练出的 LoRA 权重
runner.load_lora_weights(
    lora_weights_path="./lora_adapter_news/checkpoint-250/adapter_model.safetensors", # 请替换为实际checkpoint路径
    lora_adapter_name=ADAPTER_NAME,
)
hn_dict = runner.load_model_script(f"./models/{MODEL_NAME}.alls")

# 3. 构造量化校准数据
# 这里仅使用最基础的 Dummy Data 作为示例,实际应用中建议使用格式化后的真实文本 input_ids
import numpy as np
cache_size = hn_dict["net_params"]["cache_size"]
dummy_input = np.zeros((64, 1, cache_size)) # 64 个全零样本用于跑通编译流程
dummy_cur_pos = np.ones((64,)) * cache_size

input_dict = {
    f"{ADAPTER_NAME}/input_layer1": dummy_input,
    f"{ADAPTER_NAME}/input_layer2": dummy_cur_pos,
    f"{ADAPTER_NAME}/input_layer3": dummy_cur_pos,
    f"{ADAPTER_NAME}/input_layer4": dummy_cur_pos,
    f"{ADAPTER_NAME}/input_layer5": dummy_cur_pos,
    f"{ADAPTER_NAME}/input_layer6": dummy_cur_pos,
}

print("开始优化 LoRA 权重...")
with tf.device("/cpu:0"):
    runner.optimize(input_dict)

# 保存中间状态并重新加载以准备编译
runner.save_har(f"{MODEL_NAME}.lora.q.har", compilation_only=False)
runner = ClientRunner(hw_arch="hailo10h", har=f"{MODEL_NAME}.lora.q.har")

# 4. 替换编译脚本中的占位符
with open(f"./models/{MODEL_NAME}_compilation.alls", "r") as f:
    compile_script = f.read().replace("LORA_NAME_PLACEHOLDER", ADAPTER_NAME)

with open("compile_final.alls", "w") as f:
    f.write(compile_script)

runner.load_model_script("compile_final.alls")

# 5. 执行最终编译
print("开始编译 HEF (此过程可能需要大量内存与时间)...")
runner.compile()
runner.save_har(f"{MODEL_NAME}.lora.compiled.har", compilation_only=True)
print("编译完成!可使用 HailoRT 进行推理。")

总结

折腾完这一套之后,对端侧大模型多了一些具体的认识。虽然目前 Hailo-10 支持的模型有限,微调和编译的流程也比较繁琐。并且小模型的能力有限,RAG 能补充知识但补不了推理能力。但对于一些特定的应用场景,还是很有价值的。比如在一些对隐私敏感、网络受限的纯嵌入式场景中,根据任务结果使用本地大模型进行分析推理是非常有意义的。未来如果 Hailo 能支持用户自定义模型,或者提供更简单的微调和编译流程,相信会有更多有趣的应用出现。