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模型的基础依赖库,执行以下终端命令完成依赖一键安装:

CMD> pip install -i https://pypi.tuna.tsinghua.edu.cn/simple gguf

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

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模型绝对路径,执行以下终端命令运行脚本:

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 模型,确保本地服务已经启动。

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请求实现模型推理。

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")

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

CMD> python main.py

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

实现内存记忆

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

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

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

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")

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

CMD> python main.py

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

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

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

实现工具调用

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

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

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")

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

你:当前日期时间是多少
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岁。
posted @ 2026-08-25 16:56  lyshark  阅读(47)  评论(0)    收藏  举报