知识库效果评测

首先创建完成一个知识库,点击三个...,知识问答,如下图:

image

切换到知识检索页面,点击发布按钮,跳转到如下图:

image

复制应用ID,点击API KEY,跳转到api_key页面,点击API调试,跳转到API调试页面,如下图:

image

agent_id就是应用ID,复制api_key,输入api_key,在这个页面可以看到queryquery里输入如何创建ES实例?,点击运行按钮,运行成功,返回一个大json,再切换到知识库,显示如下图:

image

切换到知识管理页面,点击查看详情按钮,显示文档列表,可以查看切片,显示190个切片,这些切片就是数据集。有了数据集就要做评测集,评测集就是问题和答案是什么?基于数据集构造各种各样的问题,构造完问题后,要有预期答案,尽量返回结构化的想要的json字段。
点击应用管理,点击创建应用按钮,默认Agent 2.0,输入应用名称:阿里云知识库问答,点击立即创建按钮。点击配置按钮,点击模型下拉框,点击更多模型,左侧模型系列选择一个额度比较多的,如Qwen3.6-plus,点击确定按钮。点击如下图右上角的图标:

image

进入知识库页面,点击知识检索,点击配置,点击齿轮,修改相似度阈值0.6最大召回数量设置为3,点击发布按钮,输入版本描述(这个不输也行),点击确认发布,弹出发布成功。点击发布按钮右侧三个...,点击API调试,输入api key和query(如何创建ECS实例),点击运行按钮,返回一条。

弹出的页面,默认保留第一个和第三个,把第二个注释掉,点击知识库后面的+,点击添加,把知识库挂上,纯知识库外面套着大模型。短期记忆默认保持3轮。进入知识库页面,点击知识问答,点击配置,接下来在右侧输入提示词,如何创建ECS实例,点击发送按钮,返回如下图:

image

返回的内容都是从我知识库里第一个相关性最高的片段召回的,同时返回了5个片段(应该是个bug,我设置了3),只用了第一个片段,把第一个片段拆成一个步骤,没有实现多个片段去融合,返回的步骤也不适合做评测,应该返回一个json串,{content: xxx, doc_id:xxx,  product:xxx},返回这样一个格式,如query:  如何创建ECS实例.       answer: doc_id. = ecs_001,如果只有一个答案,评测集应该这样构造。实际上有召回率、准确率、命中率等,因此召回的不止一个答案,应该是一个数组。query:  如何创建ECS实例.    answer: doc_id. = [ecs_001,oss_060,rds_059],提问时知识库里有这三个片段和它是相关的,预期答案是一个数组了,标黑的就是一个评测集,可以算出召回率、准确率、命中率、MRR和F1值,接下来通过脚本实现返回的数组。

首先在Qwen3.7-千问chat里,输入那个大的返回的一个片段,输入其他的提示词(从知识库召回的都是这样格式的片段,想让大模型返回严格的json字符串,json格式字段里只返回doc_id、product、content这3个字段,提示词怎么写),点击发送,输出提示词,复制一下:

你是一个专业的文档回答助手,擅长从知识库返回内容进行回答,严格遵循以下原则:
1、禁止杜撰和自由发挥,只从知识库返回内容就行回答
2、知识库返回几个片段,结果就严格从几个片段里提取,不要截断
3、只输出一个JSON数组,不要输出任何解释、说明、markdown代码块标记(如'''json),数组中每个元素只包含以下3个字段:
- doc_id:字符串,文档ID
- product:字符串,产品名
- content:字符串,文档正文内容
如果某个片段中没有doc_id或product字段,对应值填空字符串""
4、不要输出任何额外字段,不要输出任何非JSON内容
5、如果知识库召回多个片段,每个片段都提取出doc_id、product、content

 

【输出格式示例】
[{"doc_id":"ecs_001","product":"ECS","content":"您可以通过阿里云控制台创建ECS实例..."},
{"doc_id":"ecs_002","product":"ECS","content":"登录管理控制台..."}
]

输入提示词:如何创建ECS实例,点击发送,命中了5条,和实际设置的3条不符,返回的内容符合要求。
那天只返回一条,于是换个方式,点击应用管理,点击创建应用按钮,点击工作流应用,输入应用名称:输入一个名称,如ECS测试,点击立即创建按钮,添加一个大模型知识库节点,连起来,如下图:

image

开始节点里输入api key,大模型里模式选择默认,选择一个模型配置,如Qwen3.6-Plus
知识库节点配置如下:

image

大模型节点系统提示词复制上面返回的提示词,用户提示词输入基于每次用户问题,内置变量/query 结合知识库召回的文档片段进行回答,知识库1/result
结束节点配置如下:

image

点击画布页面的测试,如下图:

image
输入提示词:如何创建ECS实例,点击发送,4个节点全部通过,但是结束节点输出为空,把文本输出改成json输出,还是输出为{},在结束节点里配置一下,如下图:

image

这下输出了json格式的字符串,

{
"result": "[
                   {\"doc_id\":\"ecs_001\",\"product\":\"ECS\",\"content\":\"您可以通过阿里云控制台创建ECS实例。登录ECS管理控制台,点击【创建实例】,选择地域、镜像、实例规格等配置后完成购买。建议新用户选择入门级实例进行测试。\"},
                  {\"doc_id\":\"ecs_019\",\"product\":\"ECS\",\"content\":\"在ECS控制台选择实例,点击【创建自定义镜像】。镜像包含系统盘数据,可用于批量部署相同环境的实例。\"},
                  {\"doc_id\":\"ecs_008\",\"product\":\"ECS\",\"content\":\"在ECS控制台,选择实例,点击【更多】-【实例设置】-【更换操作系统】。注意:此操作会删除系统盘数据,请提前备份。\"},
                  {\"doc_id\":\"ecs_012\",\"product\":\"ECS\",\"content\":\"1)在控制台创建云盘;2)将云盘挂载到ECS实例;3)登录实例,使用fdisk/parted分区,mkfs格式化,mount挂载到目录。\"},
                  {\"doc_id\":\"ecs_054\",\"product\":\"ECS\",\"content\":\"创建密钥对并绑定到ECS实例,Linux系统可使用ssh -i private_key user@ip 登录,更安全。\"}
]"
}

这下取出doc_id就相对容易了,然后在画布页面发布一下,就发布成功了,如下图:

image

有两种方式取出doc_id,一种是点击发布渠道后的立即前往,通过大模型应用ID,如下图:

image

使用应用ID:be6c1e5380644d09bf714b11cd608dd7、api key和提示词,请求api,也能和上面返回的一样。
第二种是直接测试知识库效果,没有大模型,比较纯粹。使用知识库ID:aid-0456c20d523e4a57b3d7df13278eaa87、api key和提示词,请求api,返回的是比较复杂的全部内容(没有过滤字段)。

知识库效果评测的两个步骤:
1、调通访问应用的脚本,app_id可以调用大模型应该ID,也可以调知识库ID
2、写知识库的效果评测部分

a、首先创建访问应用的脚本:
在cursor里输入

curl -X POST 'https://ws-0wabs5oka64tar3f.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search' \
-H 'Authorization: Bearer sk-ws-H.EEIMEDP.7F0C.MEYCIQCZnHF_AJmfupCTdJeO4eufNm1d_M3VagB3b64M7s-q1QIhALqVnCo2l95ywBj77B5yWSEX6H_ZkE2qntUrkrsa7PhW' \
-H 'Content-Type: application/json' \
-d '{
"query": "如何创建ECS实例",
"agent_id": "aid-0456c20d523e4a57b3d7df13278eaa87"
}' 用这种方式实现一个简单的知识库请求脚本,不要做任何发散

 

点击发送,很快生成了一个脚本kb_search.py,脚本内容如下:

import json
import urllib.request

URL = "https://ws-0wabs5oka64tar3f.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search"
API_KEY = "sk-ws-H.EEIMEDP.7F0C.MEYCIQCZnHF_AJmfupCTdJeO4eufNm1d_M3VagB3b64M7s-q1QIhALqVnCo2l95ywBj77B5yWSEX6H_ZkE2qntUrkrsa7PhW"
AGENT_ID = "aid-0456c20d523e4a57b3d7df13278eaa87"

payload = {
    "query": "如何创建ECS实例",
    "agent_id": AGENT_ID,
}

request = urllib.request.Request(
    URL,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    method="POST",
)

with urllib.request.urlopen(request) as response:
    print(response.read().decode("utf-8"))

运行该脚本,运行成功,第一步已实现。

b、生成知识库的效果评测部分的脚本
在cursor里输入:
基于 @knowledge-base-eval/eval_dataset.jsonl 该评测集进行知识库的效果评测,知识库question为每次请求的问题,doc_ids数组内容为每次从知识库预期应该召回的片段的doc_id,比如doc_ids": ["ecs_001", "ecs_054"],代表从知识库中应该召回2个片段,这2个片段的doc_id分别为ecs_001、ecs_054,基于以上进行知识库的效果评测,分别计算每次评测的召回率、准确率、命中率、MRR和F1值
点击运行,生成一个eval_kb.py文件,文件内容是:

import argparse
import json
import os
import re
import time
import urllib.error
import urllib.request
from pathlib import Path

URL = "https://ws-0wabs5oka64tar3f.cn-beijing.maas.aliyuncs.com/api/v1/indices/knowledge/search"
API_KEY = os.getenv(
    "DASHSCOPE_API_KEY",
    "sk-ws-H.EEIMEDP.7F0C.MEYCIQCZnHF_AJmfupCTdJeO4eufNm1d_M3VagB3b64M7s-q1QIhALqVnCo2l95ywBj77B5yWSEX6H_ZkE2qntUrkrsa7PhW",
)
AGENT_ID = os.getenv("KB_AGENT_ID", "aid-0456c20d523e4a57b3d7df13278eaa87")

BASE_DIR = Path(__file__).resolve().parent
DATASET_PATH = BASE_DIR / "eval_dataset.jsonl"
RESULT_PATH = BASE_DIR / "eval_results.json"
MAX_RETRIES = 3
DOC_ID_PATTERN = re.compile(r"doc_id:([^\s\\]+)")

def load_dataset(path: Path) -> list[dict]:
    text = path.read_text(encoding="utf-8").strip()
    data = json.loads(text)
    if isinstance(data, list):
        return data
    raise ValueError("评测集格式应为 JSON 数组")

def search_knowledge(query: str) -> list[str]:
    payload = {"query": query, "agent_id": AGENT_ID}
    body = json.dumps(payload).encode("utf-8")
    last_error = None

    for attempt in range(1, MAX_RETRIES + 1):
        request = urllib.request.Request(
            URL,
            data=body,
            headers={
                "Authorization": f"Bearer {API_KEY}",
                "Content-Type": "application/json",
            },
            method="POST",
        )
        try:
            with urllib.request.urlopen(request, timeout=60) as response:
                result = json.loads(response.read().decode("utf-8"))
            break
        except (urllib.error.URLError, TimeoutError, ConnectionResetError) as exc:
            last_error = exc
            if attempt == MAX_RETRIES:
                raise
            time.sleep(2 * attempt)
    else:
        raise last_error

    nodes = (((result or {}).get("data") or {}).get("nodes")) or []
    retrieved = []
    seen = set()
    for node in nodes:
        content = ((node.get("metadata") or {}).get("content")) or (node.get("text") or "")
        match = DOC_ID_PATTERN.search(content)
        if not match:
            continue
        doc_id = match.group(1)
        if doc_id in seen:
            continue
        seen.add(doc_id)
        retrieved.append(doc_id)
    return retrieved

def calc_metrics(expected: list[str], retrieved: list[str]) -> dict:
    expected_set = set(expected)
    retrieved_set = set(retrieved)
    hit_count = len(expected_set & retrieved_set)

    recall = hit_count / len(expected_set) if expected_set else 0.0
    precision = hit_count / len(retrieved_set) if retrieved_set else 0.0
    if precision + recall == 0:
        f1 = 0.0
    else:
        f1 = 2 * precision * recall / (precision + recall)

    # 命中率:预期片段中至少召回 1 个则为 1,否则为 0
    hit_rate = 1.0 if hit_count > 0 else 0.0

    # MRR:第一个命中预期片段的排名倒数
    mrr = 0.0
    for rank, doc_id in enumerate(retrieved, start=1):
        if doc_id in expected_set:
            mrr = 1.0 / rank
            break

    return {
        "recall": recall,
        "precision": precision,
        "hit_rate": hit_rate,
        "mrr": mrr,
        "f1": f1,
        "hit_count": hit_count,
        "expected_count": len(expected_set),
        "retrieved_count": len(retrieved_set),
    }

def average(values: list[float]) -> float:
    return sum(values) / len(values) if values else 0.0

def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(description="知识库召回效果评测")
    parser.add_argument(
        "-n",
        type=int,
        default=None,
        metavar="N",
        help="只评测前 N 条样本;不传则评测全量",
    )
    return parser.parse_args()


def main():
    args = parse_args()
    dataset = load_dataset(DATASET_PATH)
    if args.n is not None:
        if args.n <= 0:
            raise SystemExit("-n 必须为正整数")
        dataset = dataset[: args.n]

    total = len(dataset)
    details = []

    for index, item in enumerate(dataset, start=1):
        question = item["question"]
        expected = item["doc_ids"]
        print(f"[{index}/{total}] {question}")

        try:
            retrieved = search_knowledge(question)
            error = None
        except Exception as exc:  # noqa: BLE001 - 单条失败继续评测
            retrieved = []
            error = str(exc)
            print(f"  请求失败: {error}")

        metrics = calc_metrics(expected, retrieved)
        row = {
            "index": index,
            "question": question,
            "expected_doc_ids": expected,
            "retrieved_doc_ids": retrieved,
            "metrics": {
                "recall": round(metrics["recall"], 4),
                "precision": round(metrics["precision"], 4),
                "hit_rate": round(metrics["hit_rate"], 4),
                "mrr": round(metrics["mrr"], 4),
                "f1": round(metrics["f1"], 4),
            },
            "hit_count": metrics["hit_count"],
            "expected_count": metrics["expected_count"],
            "retrieved_count": metrics["retrieved_count"],
            "error": error,
        }
        details.append(row)
        m = row["metrics"]
        print(
            f"  召回率={m['recall']:.4f} 准确率={m['precision']:.4f} "
            f"命中率={m['hit_rate']:.4f} MRR={m['mrr']:.4f} F1={m['f1']:.4f}"
        )
        print(f"  expected={expected}")
        print(f"  retrieved={retrieved}")

    summary = {
        "sample_count": len(details),
        "avg_recall": round(average([d["metrics"]["recall"] for d in details]), 4),
        "avg_precision": round(average([d["metrics"]["precision"] for d in details]), 4),
        "avg_hit_rate": round(average([d["metrics"]["hit_rate"] for d in details]), 4),
        "avg_mrr": round(average([d["metrics"]["mrr"] for d in details]), 4),
        "avg_f1": round(average([d["metrics"]["f1"] for d in details]), 4),
    }

    output = {"summary": summary, "details": details}
    RESULT_PATH.write_text(json.dumps(output, ensure_ascii=False, indent=2), encoding="utf-8")

    print("\n===== 整体评测结果 =====")
    print(f"样本数: {summary['sample_count']}")
    print(f"平均召回率 Recall: {summary['avg_recall']:.4f}")
    print(f"平均准确率 Precision: {summary['avg_precision']:.4f}")
    print(f"平均命中率 Hit Rate: {summary['avg_hit_rate']:.4f}")
    print(f"平均 MRR: {summary['avg_mrr']:.4f}")
    print(f"平均 F1: {summary['avg_f1']:.4f}")
    print(f"明细已写入: {RESULT_PATH}")

if __name__ == "__main__":
    main()

运行方法如下:

python eval_kb.py -n 1 # 只评测前 1 条

image

 

python eval_kb.py -n 3 # 只评测前 3 条

image

 

python eval_kb.py # 全量评测

eval_dataset.jsonl的内容如下(仅截取部分内容):

image

 

做知识库指标评测的意义有哪些?特别是如果指标不好怎么办?
效果评测,如果是知识库,那就看知识库召回的效果好不好,召回率、准确率、召回的排名是否是一个正确的排名,如果不是就是算法有问题,召回的算法、相似度的算法、关键词排序的算法、知识库入库的时候分片不好,往前推数据的质量也不好,召回阶段有问题,指标不好找原因,数据集--评测集-badcase-优化-评测,达到效果就结束了,没有就继续优化,就这么一个过程,主要就是解决badcase。
挂知识库的作用:在大模型之前挂一个知识库,本意就是减少模型的幻觉,提升数据的时效性,解决缺乏私域数据。
召回的片段不对、召回的片段不全、召回的片段有问题,大模型回答的质量反而下降了,知识库就没有达到效果,所以要解决知识库的一个效果问题。
找到了badcase,优化方式有哪些?
优化是开发的工作。常见有算法的问题,1、比如语义相似度计算的算法关键词匹配算法,优化算法,换个算法或权重调一下;
2、还有可能是rerank 的算法,也要去看看;
3、还有可能切片的质量不高,有切片的算法
4、原始数据集的质量优化(把PDF转成markdown,再做一次清洗,再把格式调一调);
5、llm环节的问题,知识库返回的全且质量高,llm回答出问题了,主要解决提示词的问题

做知识库评测主要是评测数据集好不好?
1、数据集的内容好不好,原始数据内容越高、内容越好,知识库里切片的时候切的片段会好一些,但是原始数据很好、内容格式也很好,切片也切的没问题,不代表召回的时候一定被召回;
2、因为rag这有流程,先要召回再去重排,如果数据集质量好,测试还有问题,就是召回或重排算法有问题,主要是召回。内容好不代表一定被召回,内容不好召回的效果一定不好;内容好一般来说召回效果会好一些,不代表召回一定没有问题,可能会漏,内容不好召回的效果也好不了。

三者的关系总结
用一个比喻来串联:
数据集 = 课本(模型查阅的资料)
评测集 = 考试卷(一组标准测试题)
评测脚本 = 阅卷标准(怎么判分)
整个流程是:你把知识文档(数据集)喂进知识库 → 用测试题(评测集)去考系统 → 按评分规则(评测脚本)给模型的回答打分 → 发现低分的 BadCase → 针对性优化知识库切片、检索策略或 Prompt → 再次评测,形成持续优化的闭环。

 

 

 

 

 

 

 

 

 

 

posted @ 2026-08-31 14:49  laosun0204  阅读(12)  评论(0)    收藏  举报