Logo
热心市民王先生

部署实施指南

技术研究 人工智能 LLM

必需依赖 - Bun = 1.0.0(JavaScript 运行时) - SQLite(macOS 需要 Homebrew 版本以支持扩展) - 磁盘空间 至少 5GB 空闲空间(用于模型下载和数据存储) - 内存 建议 4GB+(运行本地模型需要)

4.1 环境准备

系统要求

qmd 对硬件和软件环境有以下要求:

必需依赖:

  • Bun >= 1.0.0(JavaScript 运行时)
  • SQLite(macOS 需要 Homebrew 版本以支持扩展)
  • 磁盘空间: 至少 5GB 空闲空间(用于模型下载和数据存储)
  • 内存: 建议 4GB+(运行本地模型需要)

操作系统支持:

  • macOS(推荐,需要 brew install sqlite
  • Linux(主流发行版)
  • Windows(通过 WSL2)

安装 Bun

# macOS / Linux
curl -fsSL https://bun.sh/install | bash

# 添加到 PATH
echo 'export PATH="$HOME/.bun/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# 验证安装
bun --version

安装 qmd

# 全局安装 qmd
bun install -g https://github.com/tobi/qmd

# 验证安装
qmd --version

# 确保 PATH 包含 bun 的二进制目录
export PATH="$HOME/.bun/bin:$PATH"

4.2 首次配置

步骤 1: 创建知识库目录

# 创建知识库根目录
mkdir -p ~/agent-knowledge/{projects,meetings,standards,snippets,research}

# 创建示例文档
cat > ~/agent-knowledge/standards/coding-standards.md << 'EOF'
# 代码规范

## Python 规范
- 使用 PEP 8 风格
- 类型注解必需
- 异步代码使用 async/await

## 错误处理
- 使用 try/except/finally
- 记录所有异常到日志
- 不向用户暴露内部错误
EOF

步骤 2: 配置 qmd 集合

# 添加项目文档集合
qmd collection add ~/agent-knowledge/projects --name projects
qmd context add qmd://projects "项目文档、架构设计和技术规范"

# 添加会议纪要集合
qmd collection add ~/agent-knowledge/meetings --name meetings
qmd context add qmd://meetings "团队会议、决策记录和行动项"

# 添加规范集合
qmd collection add ~/agent-knowledge/standards --name standards
qmd context add qmd://standards "编码规范、API 设计规范和安全指南"

# 添加代码片段集合
qmd collection add ~/agent-knowledge/snippets --name snippets
qmd context add qmd://snippets "可复用代码片段和最佳实践示例"

# 查看配置状态
qmd status

步骤 3: 生成向量嵌入

首次运行会下载约 2GB 的模型文件(自动从 HuggingFace 下载):

# 生成所有文档的向量嵌入
qmd embed

# 如果后续有新增文档,重新嵌入
qmd embed -f

# 查看模型缓存位置
ls -la ~/.cache/qmd/models/

模型文件说明:

  • embeddinggemma-300M-Q8_0.gguf (~300MB): 用于生成向量嵌入
  • qwen3-reranker-0.6b-q8_0.gguf (~640MB): 用于结果重排序
  • qmd-query-expansion-1.7B-q4_k_m.gguf (~1.1GB): 用于查询扩展

4.3 Agent 机器人集成代码示例

Python 集成示例(使用 subprocess)

import subprocess
import json
from typing import List, Dict, Optional

class QMDClient:
    """qmd CLI 客户端封装"""
    
    def __init__(self, collection: Optional[str] = None):
        self.collection = collection
    
    def search(self, query: str, n: int = 5) -> List[Dict]:
        """执行 BM25 关键词搜索"""
        cmd = ["qmd", "search", query, "--json", "-n", str(n)]
        if self.collection:
            cmd.extend(["-c", self.collection])
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            raise RuntimeError(f"qmd search failed: {result.stderr}")
        
        return json.loads(result.stdout)
    
    def vsearch(self, query: str, n: int = 5) -> List[Dict]:
        """执行向量语义搜索"""
        cmd = ["qmd", "vsearch", query, "--json", "-n", str(n)]
        if self.collection:
            cmd.extend(["-c", self.collection])
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return json.loads(result.stdout)
    
    def query(self, query: str, n: int = 5, min_score: float = 0.3) -> List[Dict]:
        """执行混合搜索(推荐)"""
        cmd = [
            "qmd", "query", query,
            "--json", "-n", str(n),
            "--min-score", str(min_score)
        ]
        if self.collection:
            cmd.extend(["-c", self.collection])
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return json.loads(result.stdout)
    
    def get_document(self, path: str, full_content: bool = True) -> str:
        """获取完整文档内容"""
        cmd = ["qmd", "get", path]
        if full_content:
            cmd.append("--full")
        
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.stdout
    
    def get_context_for_llm(self, query: str, max_tokens: int = 4000) -> str:
        """获取适合 LLM 上下文的检索结果"""
        results = self.query(query, n=10, min_score=0.4)
        
        context_parts = []
        current_tokens = 0
        
        for result in results:
            doc_path = result.get("path", "")
            doc_content = self.get_document(doc_path, full_content=False)
            
            # 简单估算 tokens(实际应使用 tokenizer)
            estimated_tokens = len(doc_content) / 4
            
            if current_tokens + estimated_tokens > max_tokens:
                break
            
            context_parts.append(f"## {doc_path}\n{doc_content}\n")
            current_tokens += estimated_tokens
        
        return "\n".join(context_parts)


# 在 Agent 中使用示例
class AgentWithQMD:
    def __init__(self):
        self.qmd = QMDClient()
    
    async def process_request(self, user_message: str) -> str:
        # 判断是否需要知识检索
        if self._needs_knowledge_retrieval(user_message):
            # 从 qmd 获取相关上下文
            context = self.qmd.get_context_for_llm(user_message)
            
            # 构建精简的提示词
            prompt = f"""
基于以下相关文档回答问题:

{context}

用户问题:{user_message}

请提供准确、简洁的回答。
"""
        else:
            prompt = user_message
        
        # 调用 LLM(token 消耗大幅减少)
        response = await self.call_llm(prompt)
        return response
    
    def _needs_knowledge_retrieval(self, message: str) -> bool:
        """判断是否需要知识检索"""
        keywords = ["规范", "标准", "文档", "之前", "上次", "会议", "项目"]
        return any(kw in message.lower() for kw in keywords)

Node.js/TypeScript 集成示例

import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

interface QMDResult {
  path: string;
  docid: string;
  title: string;
  score: number;
  snippet: string;
}

class QMDClient {
  constructor(private collection?: string) {}

  async search(query: string, n: number = 5): Promise<QMDResult[]> {
    let cmd = `qmd search "${query}" --json -n ${n}`;
    if (this.collection) {
      cmd += ` -c ${this.collection}`;
    }
    
    const { stdout } = await execAsync(cmd);
    return JSON.parse(stdout);
  }

  async query(query: string, n: number = 5, minScore: number = 0.3): Promise<QMDResult[]> {
    let cmd = `qmd query "${query}" --json -n ${n} --min-score ${minScore}`;
    if (this.collection) {
      cmd += ` -c ${this.collection}`;
    }
    
    const { stdout } = await execAsync(cmd);
    return JSON.parse(stdout);
  }

  async getContextForLLM(query: string, maxTokens: number = 4000): Promise<string> {
    const results = await this.query(query, 10, 0.4);
    
    const contextParts: string[] = [];
    let currentTokens = 0;

    for (const result of results) {
      const { stdout: content } = await execAsync(
        `qmd get "${result.path}" --full`
      );
      
      const estimatedTokens = content.length / 4;
      
      if (currentTokens + estimatedTokens > maxTokens) {
        break;
      }
      
      contextParts.push(`## ${result.path}\n${content}\n`);
      currentTokens += estimatedTokens;
    }

    return contextParts.join('\n');
  }
}

export default QMDClient;

4.4 MCP 服务器配置(生产环境推荐)

Claude Code 配置

编辑 ~/.claude/settings.json

{
  "mcpServers": {
    "qmd": {
      "command": "qmd",
      "args": ["mcp"]
    }
  }
}

自定义 Agent MCP 客户端配置

如果你的 Agent 使用 Python,可以使用 mcp 库:

# requirements.txt
# mcp>=1.0.0

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def use_qmd_mcp():
    # 配置 MCP 服务器参数
    server_params = StdioServerParameters(
        command="qmd",
        args=["mcp"],
        env=None
    )
    
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # 初始化连接
            await session.initialize()
            
            # 列出可用工具
            tools = await session.list_tools()
            print(f"Available tools: {[tool.name for tool in tools.tools]}")
            
            # 调用 qmd_query 工具
            result = await session.call_tool(
                "qmd_query",
                arguments={
                    "query": "user authentication best practices",
                    "n": 5,
                    "min_score": 0.3
                }
            )
            
            return result

4.5 维护与监控

定期维护任务

#!/bin/bash
# qmd-maintenance.sh

# 1. 检查索引状态
echo "=== QMD Status ==="
qmd status

# 2. 更新索引(拉取最新文档并重新索引)
echo "=== Updating Index ==="
qmd update

# 3. 重新生成向量嵌入
echo "=== Regenerating Embeddings ==="
qmd embed

# 4. 清理缓存
echo "=== Cleaning Up ==="
qmd cleanup

echo "Maintenance completed!"

添加到 crontab(每天凌晨 3 点执行)

0 3 * * * /path/to/qmd-maintenance.sh >> /var/log/qmd-maintenance.log 2>&1

监控指标

建议监控以下指标:

指标说明告警阈值
索引大小~/.cache/qmd/index.sqlite 文件大小> 1GB
查询响应时间平均 query 执行时间> 2s
Token 节省率(原始 tokens - 检索后 tokens) / 原始 tokens< 80%
检索命中率返回结果数 / 查询次数< 50%

4.6 故障排除

常见问题

问题 1: 模型下载失败

# 检查网络连接
ping huggingface.co

# 手动下载模型到 ~/.cache/qmd/models/
# 或使用镜像
export HF_ENDPOINT=https://hf-mirror.com
qmd embed

问题 2: 权限错误

# 确保目录权限正确
chmod -R 755 ~/.cache/qmd
chmod -R 755 ~/agent-knowledge

问题 3: SQLite 扩展不支持

# macOS 用户需要安装 Homebrew 的 SQLite
brew install sqlite
export PATH="/opt/homebrew/opt/sqlite/bin:$PATH"

问题 4: 搜索结果为空

# 检查集合配置
qmd collection list

# 确认文档已正确索引
qmd ls <collection-name>

# 重新索引
qmd update
qmd embed -f

4.7 需要额外下载的模型说明

qmd 会自动下载以下模型到 ~/.cache/qmd/models/

模型大小用途下载来源
embeddinggemma-300M-Q8_0.gguf~300MB生成向量嵌入HuggingFace
qwen3-reranker-0.6b-q8_0.gguf~640MB结果重排序HuggingFace
qmd-query-expansion-1.7B-q4_k_m.gguf~1.1GB查询扩展HuggingFace

首次使用需要: 网络连接以下载模型(约 2GB 数据) 后续使用: 完全离线运行,无需网络

如果无法访问 HuggingFace,可以:

  1. 使用镜像站点(如 hf-mirror.com)
  2. 手动下载模型文件并放置到正确位置
  3. 使用预配置好的 Docker 镜像

4.8 Docker 部署方案(可选)

如果需要容器化部署,可以使用社区提供的 Docker 方案:

FROM oven/bun:1

RUN bun install -g https://github.com/tobi/qmd

# 预下载模型(可选,避免运行时下载)
# COPY models/ /root/.cache/qmd/models/

VOLUME ["/app/knowledge", "/root/.cache/qmd"]

CMD ["qmd", "mcp"]
# 构建镜像
docker build -t qmd-server .

# 运行容器
docker run -d \
  --name qmd \
  -v ~/agent-knowledge:/app/knowledge \
  -v qmd-cache:/root/.cache/qmd \
  qmd-server

参考资料