Python 原生封装 Llama.cpp 大模型推理接口

本文以通义千问Qwen量化GGUF模型为案例,基于Llama.cpp框架展示本地大模型调用与功能开发的完整实现原理及流程。全文采用原生Python编写代码案例,无需依赖重型AI框架,依次实现模型元数据解析、本地接口单次对话调用、多轮对话持久化记忆、自定义工具调用拓展等功能实现逻辑,针对直接调用大模型上下文断裂、功能单一、拓展性不足等问题,通过轻量化对话记忆与工具解析执行框架完成优化实现,拆解本地对话交互运行原理,帮助开发者掌握大模型的基础调用原理。

大模型基础内容

什么是GGUF格式

首先 GGUF 是 llama.cpp 项目作者 Georgi Gerganov 于 2023 年 8 月 21 日推出的大模型文件存储格式标准,用于替代老旧的 GGML 格式。原始大模型(如 Llama、Qwen、Mistral 等)由各厂商完成训练,并以 Hugging Face PyTorch 格式发布原版权重。开发者需要借助 llama.cpp 提供的转换工具,将 HF 原版权重转换导出为 GGUF 文件,模型才能够在 llama.cpp 生态中完成本地推理运行。

与 GGML 格式不同 GGUF 格式将模型权重、分词器、各类模型配置参数全部封装打包到单个.gguf文件当中,相比前代格式,GGUF 拥有更广的兼容性、更快的模型加载速度、更低的内存开销以及更强的推理稳定性,现已成为大模型本地轻量化部署的主流优选格式。

针对 Ollama 框架,其底层推理引擎直接复用 llama.cpp,同时在 llama.cpp 的基础上封装扩展了模型拉取、版本管理、模型调用等一系列能力,降低使用门槛,便于开发者快速搭建私有化的大模型推理服务。

模型参数规模

简单理解参数量代表模型的规模,在同一模型系列、训练质量相近的前提下,参数量越大,知识储备、逻辑推理、复杂任务处理能力越强;但代价也同步上升,比如模型文件体积更大,运行需要的内存/显存更多,推理速度会变慢;但参数量也不是唯一评判标准,不同架构、不同训练数据的模型,即使参数量相同,实际效果差距也可能很大;部分经过精细微调的小模型,表现也有可能优于普通大参数量模型。

通常情况下大模型参数以Billion(亿)为单位,1B ≈10 亿参数,能够接触到并最常见的大模型参数量范围如下:

  • 1B = 1 Billion ≈10 亿参数
  • 2B = 2 Billion ≈20 亿参数
  • 3B = 3 Billion ≈30 亿参数
  • 4B = 4 Billion ≈40 亿参数
  • 7B = 7 Billion ≈70 亿参数
  • 8B = 8 Billion ≈80 亿参数
  • 14B = 14 Billion ≈140 亿参数
  • 20B = 20 Billion ≈200 亿参数
  • 34B = 34 Billion ≈340 亿参数
  • 70B = 70 Billion ≈700 亿参数

模型量化仅对权重数值做压缩处理,不会改变模型本身网络结构,模型原生能力上限保持不变,仅实际推理精度受到损耗。量化属于权衡取舍,用可接受的少量效果损失,换取更小的存储占用、更低硬件门槛以及更快推理速度。实际使用时,优先选择 Q4_K_M、Q5_K_M、Q6_K、Q8_0 这类成熟量化版本,不建议日常使用 Q2、Q3 这类低质量量化,容易产生大量幻觉和错误回答。

  • 非量化(FP16 / BF16 原版):原始精度权重,每个参数用 16 位浮点数保存。

    • 优点:效果最好,几乎无损,推理质量最高
    • 缺点:体积巨大,吃内存显存,硬件要求高。比如 8B 模型 FP16 大约 16GB。
  • 量化 GGUF(Q4_K_M、Q5_K_M、Q8_0 这类):把模型权重数值压缩,用更少比特存储一个参数。

    • 优点:文件体积大幅缩小,内存 / 显存占用降低,普通电脑也能跑;速度更快。
    • 缺点:会损失一小部分精度;量化等级越低(Q2、Q3)压缩越强,模型回答越容易胡言乱语。

若要本地部署可以参考以下参数量范围:

  • 1‑4B:低配电脑、笔记本,适合简单问答、文本摘要等轻量任务
    • 显存:最低 2‑3GB;核显也可运行
    • 系统内存:最低 8GB,推荐 16GB
    • 参考显卡:GTX1650、RTX3050,轻薄本核显可跑
  • 7‑8B:家用电脑主流选择,推理效果与硬件消耗较为均衡
    • 显存:最低 4‑6GB,推荐 8‑12GB
    • 系统内存:最低 12GB,推荐 16GB
    • 参考显卡:RTX3060 12G、RTX4060;8G 显存显卡可跑
  • 14‑20B:需要较好显存支持,逻辑推理能力相比小模型有明显提升
    • 显存:最低 9‑10GB,推荐 16GB 以上显存
    • 系统内存:最低 24GB,推荐 32GB
    • 参考显卡:RTX4060Ti‑16G、RTX3090 12G
  • 34B 及以上:硬件门槛较高,适合高性能台式机运行
    • 显存:Q4_K_M 最低 18‑20GB,推荐 24GB 及以上
    • 系统内存:最低 32GB,推荐 64GB
    • 参考显卡:RTX3090/4090(24G)、RTX5090(32G)

查询模型信息

gguf 是适配 Llama.cpp 生态的官方Python解析工具模块,专门用于读取、解析 GGUF 格式量化大模型文件,是本地离线部署GGUF模型的基础依赖库,执行以下终端命令完成依赖一键安装:

bash 复制代码
CMD> pip install -i https://pypi.tuna.tsinghua.edu.cn/simple gguf

下文提供全自动Python解析脚本,无需手动配置参数,可一键读取本地GGUF模型文件,完整输出模型基础信息、网络超参、分词器配置、张量统计、量化参数等全维度数据,适配所有主流GGUF格式大模型。

python 复制代码
import os
import sys
from gguf import GGUFReader

if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit(1)

    model_path = sys.argv[1]
    if not os.path.exists(model_path):
        print(f"错误:文件不存在 -> {model_path}")
        sys.exit(1)

    reader = GGUFReader(model_path)
    meta = {}
    for k, field in reader.fields.items():
        meta[k] = field.contents()

    file_size_bytes = os.path.getsize(model_path)
    file_size_gb = file_size_bytes / (1024 ** 3)
    arch = meta.get("general.architecture")

    GGML_TYPE_MAP = {
        0: "F32",
        1: "F16",
        2: "Q4_0",
        3: "Q4_1",
        6: "Q5_0",
        7: "Q5_1",
        8: "Q8_0",
        9: "Q8_1",
        10: "Q8_K",
        11: "Q6_K",
        12: "Q4_K",
        13: "Q5_K",
        14: "Q6_K",
        15: "Q2_K",
        16: "Q3_K",
        17: "IQ3_K_S",
        18: "IQ3_K_M",
        19: "IQ3_K_L",
        20: "IQ2_K_S",
        21: "IQ2_K_M",
    }

    model_filename = os.path.basename(model_path)
    total_param_count = 0
    for tensor in reader.tensors:
        cnt = 1
        for dim in tensor.shape:
            cnt *= dim
        total_param_count += cnt
    total_param_b = total_param_count / 1e9

    print("-" * 80)
    print("[GGUF 文件整体信息]")
    print("-" * 80)
    print(f"文件名称         : {model_filename}")
    print(f"文件大小         : {file_size_gb:.3f} GB  ({file_size_bytes:,} bytes)")
    print(f"GGUF版本         : {meta.get('general.file_version')}")
    print(f"GGUF内部version  : {meta.get('GGUF.version')}")
    print(f"文件类型(量化)   : {meta.get('general.file_type')}")
    print(f"量化版本         : {meta.get('general.quantization_version')}")
    print(f"模型原始总参数量 : {total_param_count:,} ≈ {total_param_b:.2f} B")

    print("\n" + "-" * 80)
    print("[模型基础通用信息 general.*]")
    print("-" * 80)
    print(f"模型名称         : {meta.get('general.name')}")
    print(f"模型厂商         : {meta.get('general.author')}")
    print(f"参数量标签       : {meta.get('general.size_label')}")
    print(f"模型架构         : {meta.get('general.architecture')}")
    print(f"模型版本         : {meta.get('general.version')}")
    print(f"许可 License     : {meta.get('general.license')}")
    print(f"来源URL          : {meta.get('general.source_url')}")
    print(f"来源HuggingFace  : {meta.get('general.source_hf')}")
    print(f"描述 description : {meta.get('general.description')}")
    print(f"量化工具         : {meta.get('general.quantization_tool')}")
    print(f"量化工具版本     : {meta.get('general.quantization_tool_version')}")

    print("\n" + "-" * 80)
    print(f"[模型网络超参 {arch}.*]")
    print("-" * 80)
    print(f"block_count(层数)          : {meta.get(f'{arch}.block_count')}")
    print(f"embedding_length(隐层dim)  : {meta.get(f'{arch}.embedding_length')}")
    print(f"feed_forward_length(FFN)   : {meta.get(f'{arch}.feed_forward_length')}")
    print(f"context_length(训练上下文) : {meta.get(f'{arch}.context_length')}")
    print(f"head_count(总注意力头)     : {meta.get(f'{arch}.attention.head_count')}")
    print(f"key_value_head_count(KV头) : {meta.get(f'{arch}.attention.head_count_kv')}")
    print(f"rope.dimension_count(RoPE维度): {meta.get(f'{arch}.rope.dimension_count')}")
    print(f"rope.freq_base(RoPE theta) : {meta.get(f'{arch}.rope.freq_base')}")
    print(f"rope.scaling_type          : {meta.get(f'{arch}.rope.scaling_type')}")
    print(f"rope.scaling_factor        : {meta.get(f'{arch}.rope.scaling_factor')}")
    print(f"rope.ext_factor            : {meta.get(f'{arch}.rope.ext_factor')}")
    print(f"rope.attn_factor           : {meta.get(f'{arch}.rope.attn_factor')}")
    print(f"rope.finetune              : {meta.get(f'{arch}.finetune')}")
    print(f"norm_epsilon(RMS‑eps)      : {meta.get(f'{arch}.attention.layer_norm_rms_epsilon')}")
    print(f"attention.dropout          : {meta.get(f'{arch}.attention.dropout')}")

    attn_head = meta.get(f"{arch}.attention.head_count")
    attn_kv_head = meta.get(f"{arch}.attention.head_count_kv")
    attn_norm_eps = meta.get(f"{arch}.attention.layer_norm_rms_epsilon")

    print(f"attention.head_count      : {attn_head}")
    print(f"attention.head_count_kv   : {attn_kv_head}")
    print(f"attention.layer_norm_rms_epsilon : {attn_norm_eps}")

    print(f"\n>>> MoE混合专家(普通模型为None)")
    print(f"expert_count(专家总数)     : {meta.get(f'{arch}.expert_count')}")
    print(f"expert_used_count(激活数)  : {meta.get(f'{arch}.expert_used_count')}")

    print("\n" + "-" * 80)
    print("[Tokenizer 分词器配置 tokenizer.ggml.*]")
    print("-" * 80)
    print(f"vocab_size(词表大小)       : {meta.get('tokenizer.ggml.vocab_size')}")
    print(f"BOS token id               : {meta.get('tokenizer.ggml.bos_token_id')}")
    print(f"EOS token id               : {meta.get('tokenizer.ggml.eos_token_id')}")
    print(f"PAD token id               : {meta.get('tokenizer.ggml.pad_token_id')}")
    print(f"UNK token id               : {meta.get('tokenizer.ggml.unk_token_id')}")
    print(f"SEP token id               : {meta.get('tokenizer.ggml.sep_token_id')}")
    print(f"CLS token id               : {meta.get('tokenizer.ggml.cls_token_id')}")
    print(f"MASK token id              : {meta.get('tokenizer.ggml.mask_token_id')}")

    chat_template = meta.get("tokenizer.ggml.chat_template")
    chat_template_alt = meta.get("tokenizer.chat_template")
    if chat_template:
        print("\n>>> Chat Template(Jinja2对话模板‑ggml)")
        print(chat_template)
    elif chat_template_alt:
        print("\n>>> Chat Template(Jinja2对话模板‑tokenizer)")
        print(chat_template_alt)

    special_tokens_list = meta.get("tokenizer.ggml.special_tokens")
    if special_tokens_list is not None:
        print(f"\n>>> 特殊token列表数量: {len(special_tokens_list)}")

    print("\n" + "-" * 80)
    print("[张量统计信息]")
    print("-" * 80)
    total_tensors = len(reader.tensors)
    print(f"张量总数量: {total_tensors}")

    dtype_counter = {}
    total_weights_elements = 0
    for tensor in reader.tensors:
        dtype_counter[tensor.tensor_type] = dtype_counter.get(tensor.tensor_type, 0) + 1
        ele_cnt = 1
        for s in tensor.shape:
            ele_cnt *= s
        total_weights_elements += ele_cnt

    print(f"权重元素总个数: {total_weights_elements:,}")
    print("各张量类型计数(原始数字 + 可读类型):")
    for dt, cnt in dtype_counter.items():
        dt_name = GGML_TYPE_MAP.get(dt, "UNKNOWN")
        print(f"  {dt} ({dt_name:<8}) : {cnt} 个张量")

    print("\n>>> 前8个张量信息展示:")
    for t in reader.tensors[:8]:
        shape_str = str(t.shape)
        dt_name = GGML_TYPE_MAP.get(t.tensor_type, "UNKNOWN")
        print(f"  {t.name:<55} shape:{shape_str:<25} dtype:{t.tensor_type:2d}({dt_name})")

    n_layer = meta.get(f"{arch}.block_count")
    n_embd = meta.get(f"{arch}.embedding_length")
    ff_dim = meta.get(f"{arch}.feed_forward_length")
    if n_layer and n_embd and ff_dim:
        print("\n>>> 简易Transformer层估算(不含embedding/output层,仅作对比)")
        est_params = n_layer * (2 * n_embd * n_embd + ff_dim * n_embd)
        est_billion = est_params / 1e9
        print(f"估算参数量(仅transformer层): {est_params:,} ≈ {est_billion:.2f} B")

    print("\n" + "-" * 80)
    print("[元数据字段总览]")
    print("-" * 80)
    all_meta_keys = list(meta.keys())
    print(f"GGUF文件内元数据总字段数量:{len(all_meta_keys)}")
    print("\n全部元数据key列表:")
    for key in sorted(all_meta_keys):
        print(f"  {key}")

将上述代码保存文件,放置于本地模型文件同级目录,将命令中的模型路径替换为本地GGUF模型绝对路径,执行以下终端命令运行脚本:

bash 复制代码
CMD> python main.py C://llamacpp/qwen2.5-1.5b-instruct-q4_k_m.gguf

执行接口调用

1、承接《Windows 环境下 llama.cpp 编译运行指南》部分内容,通过命令工具llama‑server启动服务,端口监听127.0.0.1:11433,并加载 qwen2.5‑1.5b‑instruct‑q4_k_m.gguf 模型,确保本地服务已经启动。

bash 复制代码
CMD> llama-server.exe -m qwen2.5-1.5b-instruct-q4_k_m.gguf --host 127.0.0.1 --port 11433 -c 4096
CMD> 
init: llama threadpool init, n_threads = 12
load_model: initializing, n_slots = 4, n_ctx_slot = 4096, kv_unified = 'true'
llama_server: model loaded
llama_server: listening on http://127.0.0.1:11433

2、完成模型解析与环境校验后,即可基于Llama.cpp本地服务实现基础对话功能。本模块全程使用Python原生urllib+json库开发,无任何第三方框架依赖,通过向本地11433端口服务发起POST请求实现模型推理。

python 复制代码
import json
import urllib.request
import urllib.error

class Config:
    LLM_BASE_URL = "http://127.0.0.1:11433"
    LLM_MODEL = "qwen2.5-1.5b-instruct-q4_k_m.gguf"
    LLM_TEMPERATURE = 0.8
    LLM_MAX_TOKENS = 1024
    LLM_CTX_SIZE = 4096
    LLM_STOP_WORDS = ["<|im_end|>"]
    TIMEOUT_SECONDS = 30

class LlamaCppClient:
    def __init__(self):
        self.base_url = Config.LLM_BASE_URL
        self.model = Config.LLM_MODEL
        self.temperature = Config.LLM_TEMPERATURE
        self.max_tokens = Config.LLM_MAX_TOKENS
        self.ctx_size = Config.LLM_CTX_SIZE
        self.stop = Config.LLM_STOP_WORDS
        self.timeout = Config.TIMEOUT_SECONDS

    def generate(self, prompt: str) -> str:
        url = f"{self.base_url}/completion"
        headers = {"Content-Type": "application/json"}
        payload = {
            "model": self.model,
            "prompt": prompt,
            "temperature": self.temperature,
            "max_tokens": self.max_tokens,
            "ctx_size": self.ctx_size,
            "stop": self.stop,
            "stream": False
        }
        try:
            req = urllib.request.Request(
                url,
                data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
                headers=headers,
                method="POST"
            )
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                return data.get("content", "").strip()
        except urllib.error.URLError as e:
            return f"[错误] llama.cpp服务连接失败:{str(e)}"
        except json.JSONDecodeError:
            return "[错误] 返回非合法JSON"
        except Exception as e:
            return f"[错误] {str(e)}"

if __name__ == "__main__":
    llm = LlamaCppClient()

    # 拼接提示词
    system_text = "你是一个专业的助手,能够回答用户的问题。"
    user_input = "你好,简单介绍一下自己"

    prompt = (
        f"<|im_start|>system\n{system_text}<|im_end|>\n"
        f"<|im_start|>user\n{user_input}<|im_end|>\n"
        f"<|im_start|>assistant\n"
    )
    
    # 调用模型生成回复
    reply = llm.generate(prompt)
    print(f"AI:{reply}\n")

执行以下终端命令运行脚本:

bash 复制代码
CMD> python main.py

AI:我是阿里云开发的一款超大规模语言模型,我叫通义千问。

实现内存记忆

上一模块的基础单次对话为无状态推理,模型无法记忆历史对话内容,多轮交互时上下文断裂、对话逻辑脱节,无法满足日常人机交互需求。

本模块自定义轻量化ChatMemory对话内存类,通过列表结构化存储系统提示词、用户提问、助手回复等完整对话记录,自动拼接标准化完整提示词,实现永久上下文记忆、手动清空记忆、持续多轮对话功能,完美模拟在线AI对话交互逻辑。

该内存方案无需数据库存储,纯内存运行、轻量化无冗余,适配本地离线部署场景,同时预留拓展接口,可后续新增文件持久化记忆功能

python 复制代码
import json
import urllib.request
import urllib.error
from typing import List, Dict

class Config:
    LLM_BASE_URL = "http://127.0.0.1:11433"
    LLM_MODEL = "qwen2.5-1.5b-instruct-q4_k_m.gguf"
    LLM_TEMPERATURE = 0.8
    LLM_MAX_TOKENS = 1024
    LLM_CTX_SIZE = 4096
    LLM_STOP_WORDS = ["<|im_end|>"]
    TIMEOUT_SECONDS = 30

class LlamaCppClient:
    def __init__(self):
        self.base_url = Config.LLM_BASE_URL
        self.model = Config.LLM_MODEL
        self.temperature = Config.LLM_TEMPERATURE
        self.max_tokens = Config.LLM_MAX_TOKENS
        self.ctx_size = Config.LLM_CTX_SIZE
        self.stop = Config.LLM_STOP_WORDS
        self.timeout = Config.TIMEOUT_SECONDS

    def generate(self, prompt: str) -> str:
        url = f"{self.base_url}/completion"
        headers = {"Content-Type": "application/json"}
        payload = {
            "model": self.model,
            "prompt": prompt,
            "temperature": self.temperature,
            "max_tokens": self.max_tokens,
            "ctx_size": self.ctx_size,
            "stop": self.stop,
            "stream": False
        }
        try:
            req = urllib.request.Request(
                url,
                data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
                headers=headers,
                method="POST"
            )
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                return data.get("content", "").strip()
        except urllib.error.URLError as e:
            return f"[错误] llama.cpp服务连接失败:{str(e)}"
        except json.JSONDecodeError:
            return "[错误] 返回非合法JSON"
        except Exception as e:
            return f"[错误] {str(e)}"

# 对话内存实现方式:基于列表的简单记忆
class ChatMemory:
    def __init__(self):
        self.history: List[Dict[str, str]] = []

    def add_user(self, content: str):
        self.history.append({"role":"user","content":content})

    def add_assistant(self, content: str):
        self.history.append({"role":"assistant","content":content})

    def clear(self):
        self.history.clear()

    def build_prompt(self, system_content:str) -> str:
        prompt = f"<|im_start|>system\n{system_content}<|im_end|>\n"
        for msg in self.history:
            prompt += f"<|im_start|>{msg['role']}\n{msg['content']}<|im_end|>\n"
        prompt += "<|im_start|>assistant\n"
        return prompt

if __name__ == "__main__":
    llm = LlamaCppClient()
    memory = ChatMemory()

    system_prompt = "你是一个专业的助手,能够回答用户的问题。"

    while True:
        user_in = input("你:").strip()
        if not user_in:
            continue
        if user_in in ("q","quit","exit","退出"):
            break
        if user_in == "clear":
            memory.clear()
            print("已清空对话记忆\n")
            continue

        memory.add_user(user_in)

        # 构建提示词
        prompt = memory.build_prompt(system_prompt)

        # 调用LLM生成回复
        resp = llm.generate(prompt)

        # 写入内存
        memory.add_assistant(resp)
        print(f"AI:{resp}\n")

执行以下终端命令运行脚本:

bash 复制代码
CMD> python main.py

你:你好,我叫王瑞
AI:你好,王瑞,很高兴认识你。

你:我叫什么,你知道吗
AI:你好,王瑞,很高兴认识你。

你:简单介绍下你自己
AI:我是来自阿里的智能机器人。我叫"小助手"。我可以回答各种各样的问题,提供你想要的信息。你有什么问题想问我吗?

实现工具调用

原生本地大模型存在天然能力短板:无法获取实时系统时间、无法精准运算复杂数学公式、无外部数据获取能力、静态知识存在滞后性,无法适配实用化落地场景。本模块基于前文记忆对话框架,拓展自定义工具调用系统,内置时间查询、数学计算器两大高频实用工具,通过正则匹配解析模型输出的标准化工具调用指令,自动执行本地工具函数、获取结果后二次调用模型,生成贴合用户需求的最终自然语言回复,完整实现「模型决策调用工具-本地执行工具-结果反馈模型」的闭环能力。

本工具框架高度可拓展,开发者可基于现有代码,快速新增天气查询、文件读取、文本翻译、代码运行、联网搜索等各类自定义工具,无需重构核心逻辑。同时兼容Qwen模型原生工具调用模板。

python 复制代码
import json
import urllib.request
import urllib.error
import re
from typing import List, Dict, Any
from datetime import datetime

class Config:
    LLM_BASE_URL = "http://127.0.0.1:11433"
    LLM_MODEL = "qwen2.5-1.5b-instruct-q4_k_m.gguf"
    LLM_TEMPERATURE = 0.7
    LLM_MAX_TOKENS = 1024
    LLM_CTX_SIZE = 4096
    LLM_STOP_WORDS = ["<|im_end|>"]
    TIMEOUT_SECONDS = 30

class LlamaCppClient:
    def __init__(self):
        self.base_url = Config.LLM_BASE_URL
        self.model = Config.LLM_MODEL
        self.temperature = Config.LLM_TEMPERATURE
        self.max_tokens = Config.LLM_MAX_TOKENS
        self.ctx_size = Config.LLM_CTX_SIZE
        self.stop = Config.LLM_STOP_WORDS
        self.timeout = Config.TIMEOUT_SECONDS

    def generate(self, prompt: str) -> str:
        url = f"{self.base_url}/completion"
        headers = {"Content-Type": "application/json"}
        payload = {
            "model": self.model,
            "prompt": prompt,
            "temperature": self.temperature,
            "max_tokens": self.max_tokens,
            "ctx_size": self.ctx_size,
            "stop": self.stop,
            "stream": False
        }
        try:
            req = urllib.request.Request(
                url,
                data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
                headers=headers,
                method="POST"
            )
            with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                data = json.loads(resp.read().decode("utf-8"))
                return data.get("content", "").strip()
        except urllib.error.URLError as e:
            return f"[错误] llama.cpp服务连接失败:{str(e)}"
        except json.JSONDecodeError:
            return "[错误] 返回非合法JSON"
        except Exception as e:
            return f"[错误] {str(e)}"

class ChatMemory:
    def __init__(self):
        self.history: List[Dict[str, Any]] = []

    def add_user(self, content: str):
        self.history.append({"role": "user", "content": content})

    def add_assistant(self, content: str):
        self.history.append({"role": "assistant", "content": content})

    def add_tool_response(self, content: str):
        self.history.append({"role": "tool", "content": content})

    def clear(self):
        self.history.clear()

    def build_prompt(self, system_content: str, tools: List[Dict] = None) -> str:
        prompt_parts = []
        if tools:
            tool_str = json.dumps(tools, ensure_ascii=False, indent=2)
            system_block = (
                f"{system_content}\n\n# 工具\n\n"
                "你可以调用一个或多个函数来协助回答用户的问题。\n\n"
                "工具定义放在 <tools></tools> 标签内:\n"
                f"<tools>\n{tool_str}\n</tools>\n\n"
                "调用工具时,请把函数名和参数的JSON放在 <tool_call></tool_call> 标签内部:\n"
                "<tool_call>\n{\"name\": \"函数名\", \"arguments\": {参数对象}}\n</tool_call>"
            )
            prompt_parts.append(f"<|im_start|>system\n{system_block}<|im_end|>\n")
        else:
            prompt_parts.append(f"<|im_start|>system\n{system_content}<|im_end|>\n")

        for msg in self.history:
            role = msg["role"]
            content = msg["content"]
            if role == "tool":
                prompt_parts.append(f"<|im_start|>user\n<tool_response>\n{content}\n</tool_response><|im_end|>\n")
            else:
                prompt_parts.append(f"<|im_start|>{role}\n{content}<|im_end|>\n")

        prompt_parts.append("<|im_start|>assistant\n")
        return "".join(prompt_parts)

# ---------------- 工具定义与本地实现 ----------------
TOOLS_DEFINITION = [
    {
        "type": "function",
        "function": {
            "name": "get_datetime",
            "description": "获取系统当前日期和时间,不需要参数",
            "parameters": {
                "type": "object",
                "properties": {},
                "required": []
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "计算器,执行数学运算",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {"type": "string", "description": "数学表达式,例如 100+200"}
                },
                "required": ["expression"]
            }
        }
    }
]

def tool_get_datetime() -> str:
    """获取系统当前日期时间"""
    now = datetime.now()
    return f"当前系统时间:{now.strftime('%Y‑%m‑%d %H:%M:%S')}"

def tool_calculator(expression: str) -> str:
    """简易计算器,做表达式计算"""
    try:
        res = eval(expression)
        return f"计算结果:{res}"
    except Exception as e:
        return f"计算错误:{str(e)}"

TOOL_MAP = {
    "get_datetime": tool_get_datetime,
    "calculator": tool_calculator
}

def parse_tool_calls(assistant_text: str) -> List[Dict]:
    pattern = r"<tool_call>(.*?)</tool_call>"
    matches = re.findall(pattern, assistant_text, flags=re.DOTALL)
    calls = []
    for m in matches:
        m = m.strip()
        try:
            obj = json.loads(m)
            calls.append(obj)
        except json.JSONDecodeError:
            continue
    return calls

def execute_tool_calls(tool_calls: List[Dict]) -> str:
    outputs = []
    for call in tool_calls:
        fn_name = call.get("name")
        args = call.get("arguments", {})
        if fn_name in TOOL_MAP:
            fn = TOOL_MAP[fn_name]
            result = fn(**args)
            outputs.append(f"[工具:{fn_name}] {result}")
        else:
            outputs.append(f"[工具:{fn_name}] 工具不存在")
    return "\n".join(outputs)

if __name__ == "__main__":
    llm = LlamaCppClient()
    memory = ChatMemory()
    system_prompt = "你是一个专业的助手,能够回答用户的问题,需要时调用工具。"

    while True:
        user_in = input("你:").strip()
        if user_in in ("q", "quit", "exit", "退出"):
            break
        if user_in == "clear":
            memory.clear()
            print("已清空对话记忆\n")
            continue
        if not user_in:
            continue

        memory.add_user(user_in)

        prompt = memory.build_prompt(system_prompt, tools=TOOLS_DEFINITION)
        resp = llm.generate(prompt)

        tool_calls = parse_tool_calls(resp)

        if tool_calls:
            print(f"AI(调用工具):\n{resp}\n")
            memory.add_assistant(resp)
            tool_result = execute_tool_calls(tool_calls)
            print(f"工具返回:\n{tool_result}\n")
            memory.add_tool_response(tool_result)

            prompt2 = memory.build_prompt(system_prompt, tools=TOOLS_DEFINITION)
            final_resp = llm.generate(prompt2)
            memory.add_assistant(final_resp)
            print(f"AI:{final_resp}\n")
        else:
            memory.add_assistant(resp)
            print(f"AI:{resp}\n")

执行以下终端命令运行脚本:

bash 复制代码
你:当前日期时间是多少
AI(调用工具):
<tool_call>
{"name": "get_datetime", "arguments": {}}
</tool_call>

工具返回:
[工具:get_datetime] 当前系统时间:2026‑08‑25 14:51:35

AI:当前系统时间是2026‑08‑25 14:51:35。

你:计算100+100*10
AI(调用工具):
<tool_call>
{"name": "calculator", "arguments": {"expression": "100+100*10"}}
</tool_call>

工具返回:
[工具:calculator] 计算结果:1100

AI:计算结果是1100。

你:我是王瑞,今年28岁,男性
AI:好的,王瑞先生,今年28岁,男性。

你:简单介绍下我自己
AI:王瑞,男,28岁。
相关推荐
lyshark3 天前
基于 HuggingFace Tokenizers 训练自定义分词器
大模型应用技术实践
lyshark4 天前
基于 vLLM+Nginx 构建负载均衡推理集群
大模型应用技术实践
lyshark5 天前
LLama-Factory 实现大模型LoRA-SFT微调指南
大模型应用技术实践
lyshark7 天前
千问大模型完整RLHF全参数微调指南
大模型应用技术实践
lyshark9 天前
千问大模型二次LoRA‑SFT指令微调指南
大模型应用技术实践
lyshark15 天前
轻量化小模型MiniMind从训练到落地指南
大模型应用技术实践
lyshark19 天前
Ubuntu 大模型HF转GGUF全流程实践指南
大模型应用技术实践·linux 系统运维技术实践
lyshark20 天前
LangChain 消息流输出与结构化处理
大模型应用技术实践
lyshark22 天前
LangGraph Server Agent 框架本地部署指南
大模型应用技术实践