关键词抽取与多路召回

落地的是最前面的四个节点:

extract_keywords  # 从用户问题里抽取关键词
recall_column     # 根据关键词召回可能相关的字段信息
recall_metric     # 根据关键词召回可能相关的指标信息
recall_value      # 根据关键词召回可能相关的字段真实取值

比如问题: 统计华北地区的销售总额

至少包含三类信息:

  • 销售总额:可能是一个业务指标,例如 GMV、成交额、订单金额总和;
  • 华北地区:可能是数据库字段里的真实取值,例如 华北
  • 地区:可能对应一个维度字段,例如 dim_region.region_name

所以输入问题后,从节点extract_keywords出来之后,工作流会并行进入三条召回链路:

extract_keywords
  -> recall_column  # 字段信息召回
  -> recall_metric  # 指标信息召回
  -> recall_value   # 字段取值召回

三路召回分别解决什么

1. 解决查那一列

字段召回要解决的是:用户口语里的概念,对应数仓里的哪些字段。所以字段召回回答的是:这个问题可能涉及哪些表字段?

2. 解决怎么算 

销售总额客单价GMV转正率 更像指标。指标不是普通字段,它通常有业务口径,

例如:

  • 指标名称:GMV
  • 指标描述:所有订单成交金额总和
  • 指标别名:成交额、交易额、销售总额
  • 依赖字段:fact_order.order_amount

所以指标召回回答的是:这个问题里的度量目标应该如何计算?

3. 解决条件值怎么写

华北黄金会员数码在职实习生 更像字段真实取值。

字段取值召回要解决的是:当用户说“华北地区”时,系统尽量知道数据库里真实存在的值是什么。

比如要找华北地区的销售额,正确的是where dim_region.region_name = '华北' 而不是where dim_region.region_name = '华北地区'

所以字段取值召回回答的是:过滤条件里的值应该尽量写成数据库真实存在的值。

4. 召回的分工

召回链路解决的问题检索对象技术实现
字段召回 “地区”像哪个字段? ColumnInfo Qdrant 向量检索
指标召回 “销售总额”像哪个指标? MetricInfo Qdrant 向量检索
字段取值召回 “华北地区”在库里真实值是什么? ValueInfo Elasticsearch 全文检索
  • 字段和指标都使用向量检索,是因为它们主要匹配的是语义相似
  • 字段取值使用全文检索,是因为字段取值往往是短文本、枚举值、名称、标签

 

补齐State,context和图结构

DataAgentState: 保存业务中间结果

至少需要五类状态:

  • query:用户原始问题;
  • keywords:抽取出的关键词;
  • retrieved_column_infos:召回到的字段信息;
  • retrieved_metric_infos:召回到的指标信息;
  • retrieved_value_infos:召回到的字段取值。
from typing import TypedDict

from app.entities.column_info import ColumnInfo
from app.entities.metric_info import MetricInfo
from app.entities.value_info import ValueInfo


class DataAgentState(TypedDict):
    """一次问数链路中的核心状态"""

    query: str  # 用户输入的查询
    keywords: list[str]  # 抽取的关键词
    retrieved_column_infos: list[ColumnInfo]  # 检索到的字段信息
    retrieved_metric_infos: list[MetricInfo]  # 检索到的指标信息
    retrieved_value_infos: list[ValueInfo]  # 检索到的取值信息
    error: str  # 校验 SQL 时出现的错误信息

DataAgentState: 保存运行时工具

召回节点需要访问外部系统:

  • 字段召回需要 Qdrant 字段仓储和 Embedding 客户端;
  • 指标召回需要 Qdrant 指标仓储和 Embedding 客户端;
  • 字段取值召回需要 Elasticsearch 字段值仓储。
from typing import TypedDict

from langchain_huggingface import HuggingFaceEndpointEmbeddings

from app.repositories.es.value_es_repository import ValueESRepository
from app.repositories.qdrant.column_qdrant_repository import ColumnQdrantRepository
from app.repositories.qdrant.metric_qdrant_repository import MetricQdrantRepository


class DataAgentContext(TypedDict):
    """LangGraph Runtime 中传递的上下文对象"""

    # 字段向量仓储,负责根据向量从 Qdrant 检索候选字段
    column_qdrant_repository: ColumnQdrantRepository
    # Embedding 客户端,字段召回和指标召回都会复用
    embedding_client: HuggingFaceEndpointEmbeddings
    # 指标向量仓储,负责从 Qdrant 检索候选指标
    metric_qdrant_repository: MetricQdrantRepository
    # 字段取值仓储,负责从 Elasticsearch 检索真实字段值
    value_es_repository: ValueESRepository

在graph.py中接入三路召回

用实例化graph_builder.add_node(),将extract_keywords,recall_column,recall_value,recall_metric注册节点,连接边,再统一合并到下一个节点

# 三路并行
graph_builder.add_edge("extract_keywords", "recall_column")
graph_builder.add_edge("extract_keywords", "recall_value")
graph_builder.add_edge("extract_keywords", "recall_metric")

# 三路召回都完成后,再进入统一的信息合并节点
graph_builder.add_edge("recall_column", "merge_retrieved_info")
graph_builder.add_edge("recall_value", "merge_retrieved_info")
graph_builder.add_edge("recall_metric", "merge_retrieved_info")

实现关键词抽取节点

extract_keywords 是开始的第一个节点,主要功能是:

  • 读取 state["query"]
  •  使用 jieba 提取关键词
  • 按词性过滤无关词
  • 把原始 query 加入关键词列表
  • 去重
  • 返回 {"keywords": keywords}

 

 

import jieba.analyse
from langgraph.runtime import Runtime

from app.agent.context import DataAgentContext
from app.agent.state import DataAgentState
from app.core.log import logger


async def extract_keywords(state: DataAgentState, runtime: Runtime[DataAgentContext]):
    writer = runtime.stream_writer
    # 通过 stream_writer 输出节点进度,方便本地调试或前端展示 Agent 执行状态
    writer("抽取关键词")

    query = state["query"]

    # 只保留更可能承载业务含义的词性,减少“的、帮我、一下”这类无检索价值的噪声
    allow_pos = (
        "n",   # 名词: 商品、订单、销售额
        "nr",  # 人名: 张三、李四
        "ns",  # 地名: 华北、北京、上海
        "nt",  # 机构团体名: 门店、品牌、渠道
        "nz",  # 其他专有名词: SKU、GMV、AOV
        "v",   # 动词: 统计、对比、查询
        "vn",  # 名动词: 销售、成交、退款
        "a",   # 形容词: 新增、有效、活跃
        "an",  # 名形词: 可用、有效、异常
        "eng", # 英文: GMV、SKU、ROI
        "i",   # 成语或习用语,避免遗漏整体表达
        "l",   # 常用固定短语,例如“销售总额”
    )

    # extract_tags 会基于 TF-IDF 抽取关键词,并按 allowPOS 做词性过滤
    keywords = jieba.analyse.extract_tags(query, allowPOS=allow_pos)

    # 保留原始问题作为兜底检索入口,避免关键词切分不准时丢掉完整语义
    keywords = list(set(keywords + [query]))
    logger.info(f"抽取关键词: {keywords}")
    return {"keywords": keywords}

1. jieba做了什么

jieba 本质上是一个中文分词工具。它可以把一段中文文本拆成词,也提供关键词抽取能力。

仓库地址:https://github.com/fxsjy/jieba

jieba.analyse.extract_tags(query, allowPOS=allow_pos)

extract_tags(...) 可以基于文本抽取关键词。值得关注的是 allowPOSPOS 指的是词性,allowPOS 的意思是:只保留指定词性的词

本项目保留的词性大致可以分成几类:

  • 名词、专有名词:通常可能对应字段、业务对象、指标概念;
  • 地名、机构名:可能对应地区、门店、组织等维度;
  • 英文:可能对应 GMVAOV 等指标或字段别名;
  • 动词、名动词:有时能帮助理解统计动作或业务行为;
  • 常用固定短语:一些业务短语可能整体更有意义。

当然,关键词抽取不是完美的语义理解。它只是给后续召回提供第一批入口。因此代码里还会把原始问题也加进去:

keywords = list(set(keywords + [query]))
  • keywords + [query]:把原始问题加入关键词列表;
  • set(...):去掉重复关键词。

统一封装LLM:

from langchain.chat_models import init_chat_model
from app.conf.app_config import app_config

llm = init_chat_model(
    model=app_config.llm.model_name,
    model_provider="openai",
    base_url=app_config.llm.base_url,
    api_key=app_config.llm.api_key,
    # 召回扩展、SQL 生成更看重稳定性,所以这里关闭随机发散
    temperature=0,
)

if __name__ == "__main__":
    # 单独运行本文件时,用一个最小请求验证模型名、密钥和 base_url 是否配置正确
    print(llm.invoke("你好").content)
    

模型相关的这些信息统一写在 app_config.yaml 的 llm 配置项(app_config.yaml)里:

llm:
  model_name: Pro/zai-org/GLM-5.1
  # 从本地 .env 读取,避免把真实密钥写进仓库
  api_key: ${oc.env:LLM_API_KEY}
  base_url: https://api.siliconflow.cn/v1

这里的 LLM_API_KEY 需要写在本地 .env 文件里。写教程或提交代码时,不建议把真实 API Key 直接提交到公开仓库

model_provider="openai" 不一定表示只能用 OpenAI 官方服务。很多模型服务商会兼容 OpenAI 接口格式,所以这里可以按 OpenAI 协议接入

集中管理提示词:

项目里把提示词集中放在 prompts/ 目录,然后通过 prompt_loader.py 读取。

from pathlib import Path


def load_prompt(name: str) -> str:
    """读取指定名称的 prompt 模板内容"""

    # app/prompt/prompt_loader.py 向上两级回到项目根目录,再进入 prompts 目录
    prompt_path = Path(__file__).parents[2] / "prompts" / f"{name}.prompt"
    return prompt_path.read_text(encoding="utf-8")

它们的输出都要求是 JSON 数组。这样后面可以使用 JsonOutputParser 把模型输出直接解析成 Python 列表,不需要额外清洗解释文字

LECL最小链路:

三路召回里都会看到这段相似代码:

prompt = PromptTemplate(
    template=load_prompt("extend_keywords_for_column_recall"),
    input_variables=["query"],
)
output_parser = JsonOutputParser()
chain = prompt | llm | output_parser

result = await chain.ainvoke({"query": query})

所以这条链不是三行互不相关的代码,而是一条连续的小流水线:

  • PromptTemplate 负责把变量填进提示词;
  • llm 负责根据提示词生成模型输出;
  • JsonOutputParser 负责把模型输出解析成程序更好处理的数据结构。

这和第 10 章讲过的 LangGraph 有一点相似:它们都在描述“步骤之间怎么连接”。区别在于,LCEL 更适合表达一条较短的模型调用链,比如“提示词 -> 模型 -> 解析器”;LangGraph 更适合表达完整智能体流程,比如多节点、并行召回、条件分支、流式进度输出

字段信息召回-recall_column

字段召回的目标是找到一批可能相关的 ColumnInfo。它的完整流程如下:

读取 query 和 keywords
  -> 使用 LLM 扩展“字段召回”关键词
  -> 合并原始关键词和扩展关键词
  -> 对每个关键词做 Embedding
  -> 查询 Qdrant 字段 collection
  -> 将 payload 还原为 ColumnInfo
  -> 按 column_id 去重
  -> 写入 state["retrieved_column_infos"]
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langgraph.runtime import Runtime

from app.agent.context import DataAgentContext
from app.agent.llm import llm
from app.agent.state import DataAgentState
from app.core.log import logger
from app.entities.column_info import ColumnInfo
from app.prompt.prompt_loader import load_prompt

async def recall_column(state: DataAgentState, runtime: Runtime[DataAgentContext]):
    writer = runtime.stream_writer
    writer("召回字段信息")
    # state 保存图内业务中间结果:原始问题和上游抽取出的关键词
    keywords = state["keywords"]
    query = state["query"]
    # context 保存外部运行时工具:向量仓储和 Embedding 客户端
    column_qdrant_repository = runtime.context["column_qdrant_repository"]
    embedding_client = runtime.context["embedding_client"]
    # 用 LLM 把用户问法扩展成“字段语义”列表,例如“销售总额”可扩展出“销售金额”
    propmt = PromptTemplate(
        template = load_prompt("extend_keywords_for_column_recall"),
        input_variables=["query"],
    )
    output_parser = JsonOutputParser()
    chain = prompt | llm | output_parser
    
    result = await chain.ainvoke({"query": query})
    # 原始关键词和 LLM 扩展词一起参与召回;set 去重,避免重复请求同一关键词
    keywords = set(keywords + result)
    
    # 用字段 id 做唯一键,因为多个关键词、同一字段的多个向量点都可能命中同一个字段
    column_info_map: dict[str, ColumnInfo] = {}
    
    for keyword in keywords:
        # 查询词必须先转成向量,才能和第 9 章写入 Qdrant 的字段向量做相似度检索
        embedding = await embedding_client.aembed_query(keyword)
        current_column_infos: list[ColumnInfo] =  await column_qdrant_repository.search(
            embedding
        )
        for column_info in current_column_infos:
            if column_info.id not in column_info_map:
                column_info_map[column_info.id] = column_info

    # 写回 state 的是去重后的 ColumnInfo 列表,不暴露 Qdrant 原始 point 结构
    retrieved_column_infos: list[ColumnInfo] = list(column_info_map.values())

    logger.info(f"检索到字段信息:{list(column_info_map.keys())}")
    return {"retrieved_column_infos": retrieved_column_infos}
        
   

 字段召回-关键词扩展

先用字段召回提示词做一次语义扩展:

prompt = PromptTemplate(
    template=load_prompt("extend_keywords_for_column_recall"),
    input_variables=["query"],
)
output_parser = JsonOutputParser()
chain = prompt | llm | output_parser

result = await chain.ainvoke({"query": query})

字段召回提示词的重点不是让模型生成 SQL,而是让模型输出更像“字段概念”的短语。比如模型可能输出:

["地区", "销售金额", "订单金额"]

然后再把原始关键词和扩展词合并:

keywords = set(keywords + result)

这样做的好处是:既保留用户原话作为兜底,又补充更贴近字段元数据的表达,召回范围会更稳。

 

关键字如何变成字段候选

字段信息在写入了 Qdrant字段名、字段描述、字段别名都被转换成向量;查询时,关键词也必须转换成向量,才能在同一个向量空间里做相似度检索。

for keyword in keywords:
    # aembed_query(text) 异步生成查询(query)的语义嵌入向量, 用于将单个文本字符串转换为向量表示
    embedding = await embedding_client.aembed_query(keyword)
    current_column_infos: list[ColumnInfo] = await column_qdrant_repository.search(
        embedding
    )

 如果后续关键词数量变多,可以把 Embedding 改成批量处理,减少 Embedding 请求次数:

 

keyword_list = list(keywords)
embeddings: list[list[float]] = await embedding_client.aembed_documents(
    keyword_list
)
column_info_map: dict[str, ColumnInfo] = {}
for keyword, embedding in zip(keyword_list, embeddings):
    current_column_infos: list[ColumnInfo] = await column_qdrant_repository.search(
        embedding
    )
    # 去重
    for column_info in current_column_infos:
        if column_info.id not in column_info_map:
             column_info_map[column_info.id] = column_info
                
                

 为什么按字段ID去重:

column_info_map: dict[str, ColumnInfo] = {}

for column_info in current_column_infos:
    if column_info.id not in column_info_map:
        column_info_map[column_info.id] = column_info

 这里一定要去重,因为重复来源至少有两种。

  1. 多个关键词可能召回同一个字段
    1. 例如用户问“统计东北地区和华北地区的销售总额”,“东北地区”和“华北地区”都可能召回地区字段
    2. 如果不去重,后续上下文里就会重复出现同一个字段
  2. 同一个关键词也可能命中同一个字段的多个向量点
    1. 一个字段不是只写一个 point,而是会按字段名、字段描述、字段别名拆成多个 point
    2. 如果一个关键词同时命中其中多个 point,返回的字段信息也会重复  

 所以这里不能简单 extend 到列表里,而是要先按字段 id 做唯一键,最后再取出字典里的值:

retrieved_column_infos: list[ColumnInfo] = list(column_info_map.values())

Repository只负责查询和还原字段实体

字段召回真正查询 Qdrant 的逻辑封装在 Repository 中。

async def search(
    self, embedding: list[float], score_threshold: float = 0.6, limit: int = 20
) -> list[ColumnInfo]:
    """按向量相似度检索字段元数据,并还原为 ColumnInfo 实体"""

    result = await self.client.query_points(
        collection_name=self.collection_name,
        query=embedding,
        limit=limit,
        score_threshold=score_threshold,
    )
    # Qdrant 只保存字段元数据 payload,业务层继续使用 ColumnInfo
    return [ColumnInfo(**point.payload) for point in result.points]

这里的职责很克制:节点负责组织召回流程,Repository 负责屏蔽 Qdrant 查询细节。调用方只需要传入关键词向量,拿回 list[ColumnInfo]

几个参数的含义如下:

  • collection_name:字段向量集合,这里是 column_info_collection
  • query=embedding:使用关键词向量做相似度查询;
  • score_threshold=0.6:过滤相似度太低的结果;
  • limit=20:单次最多返回 20 条候选字段。

构建字段向量索引时,完整字段信息已经作为 payload 写入 Qdrant。因此这里命中向量点后,可以直接用:ColumnInfo(**point.payload)把 payload 还原成业务层使用的 ColumnInfo

指标信息召回recall_metric

指标召回的逻辑和字段召回非常像,只是检索目标从字段 collection 换成了指标 collection。

整体流程如下:

读取 query 和 keywords
  -> 使用 LLM 扩展“指标召回”关键词
  -> 合并原始关键词和扩展关键词
  -> 对每个关键词做 Embedding
  -> 查询 Qdrant 指标 collection
  -> 将 payload 还原为 MetricInfo
  -> 按 metric_id 去重
  -> 写入 state["retrieved_metric_infos"]

recall_metric核心代码

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langgraph.runtime import Runtime

from app.agent.context import DataAgentContext
from app.agent.llm import llm
from app.agent.state import DataAgentState
from app.entities.metric_info import MetricInfo
from app.prompt.prompt_loader import load_prompt
from app.core.log import logger

async def recall_metric(state: DataAgentState, runtime: Runtime[DataAgentContext]):
    writer = runtime.stream_writer
    writer("召回指标信息")
    query = state["query"]
    keywords = state["keywords"]
    embedding_client = runtime.context["embedding_client"]
    metric_qdrant_repository = runtime.context["metric_qdrant_repository"]
    
    # 指标扩展关注“怎么算”,例如销售总额可扩展成 GMV、成交额、交易额
    prompt = PromptTemplate(
        template=load_prompt("extend_keywords_for_metric_recall"),
        input_variables=["query"],
    )
    output_parser = JsonOutputParser()
    chain = prompt | llm | output_parser
    
    for keyword in keywords:
        embedding = await embedding_client.aembed_query(keyword)
        current_metric_infos: list[MetricInfo] = await metric_qdrant_repository.search(
            embedding
        )
        for metric_info in current_metric_infos:
            if metric_info.id not in metric_info_map:
                metric_info_map[metric_info.id] = metric_info
    retrieved_metric_infos: list[MetricInfo] = list(metric_info_map.values())
    logger.info(f"检索到指标信息:{list(metric_info_map.keys())}")
    
    return {"retrieved_metric_infos": retrieved_metric_infos}
    
    

这段代码和 recall_column 的结构几乎一致: PromptTemplate -> LLM -> JsonOutputParser       关键词 -> Embedding -> Qdrant -> 去重

差异在于:

  • 提示词换成 extend_keywords_for_metric_recall
  • Repository 换成 metric_qdrant_repository
  • 返回结果换成 retrieved_metric_infos
  • 去重对象从 ColumnInfo 换成 MetricInfo

MetricQdrantRepository.search

class MetricQdrantRepository:
    collection_name = "metric_info_collection"
    
    def __init__(self, client: AsyncQdrantClient):
        self.client = client
    async def search(
        self,
        embedding: list[float],
        score_threshold: float = 0.6,
        limit: int = 20,
    ) -> list[MetricInfo]:
         result = await self.client.query_points(
             collection_name = self.collection_name,
             query = embedding,
             limit = limit,
             score_threshold= score_threshold
         )
         return [MetricInfo(**point.payload) for point in result.points]   
    

这个 Repository 方法的职责很单一:给它一个向量,它从指标 collection 里返回一批 MetricInfo

指标实体:

from dataclasses import dataclass


@dataclass
class MetricInfo:
    id: str
    name: str
    description: str
    relevant_columns: list[str]
    alias: list[str]

字段取值召回: recall_value

字段取值召回找的是“数据库里真实存在的值”。因此它不需要 Embedding,也不查询 Qdrant,而是查询前面已经构建好的 Elasticsearch 字段值索引

整体流程如下:

读取 query 和 keywords
  -> 使用 LLM 扩展“字段取值”关键词
  -> 合并原始关键词和扩展关键词
  -> 对每个关键词查询 Elasticsearch
  -> 将命中的 _source 还原为 ValueInfo
  -> 按 value_id 去重
  -> 写入 state["retrieved_value_infos"]

recall_value核心代码

from langchain_core.output_parsers import JsonOutputParser
from langchain_core.prompts import PromptTemplate
from langgraph.runtime import Runtime

from app.agent.context import DataAgentContext
from app.agent.llm import llm
from app.agent.state import DataAgentState
from app.entities.value_info import ValueInfo
from app.prompt.prompt_loader import load_prompt
from app.core.log import logger

async def recall_value(state: DataAgentState, runtime: Runtime[DataAgentContext]):
    writer = runtime.stream_writer
    writer("召回字段取值")
    query = state["query"]
    keywords = state["keywords"]
    # 字段取值走 Elasticsearch,不需要 embedding_client
    value_es_repository = runtime.context["value_es_repository"]
    # 取值扩展关注“真实值”,例如华北地区可扩展出华北
    prompt = PromptTemplate(
        template = load_prompt("extend_keywords_for_value_recall")
        input_variables= ["query"]
    )    
    output_parser = JsonOutputParser()
    chain = prompt | llm | output_parser
    result = await chain.invoke({"query": query})
    keyword = set(keywords + result)
    
    value_infos_map = dict[str, ValueInfo] = {}
    for keyword in keywords:
         # 直接用关键词查 Elasticsearch 字段值索引
        current_value_infos: list[ValueInfo] =  await value_es_repository.search(keyword)
        for current_value_info in current_value_infos:
            if current_value_info.id not in value_infos_map:
                 value_infos_map[current_value_info.id] = current_value_info
                    
    retrieved_value_infos: list[ValueInfo] = list(value_info_map.values())
    logger.info(f"检索到字段取值:{list(value_infos_map.keys())}")
    return {"retrieved_value_infos": retrieved_value_infos}

和 recall_metric 对比一下,会发现差异很集中:

recall_metric:
  keyword -> embedding -> Qdrant -> MetricInfo

recall_value:
  keyword -> Elasticsearch match query -> ValueInfo

ValueInfo结构:

from dataclasses import dataclass


@dataclass
class ValueInfo:
    id: str    # 唯一标识
    value: str    # 真实字段值
    column_id: str # 这个值属于哪个字段

value_es_repository.search()

class ValueESRepository:
    index_name = "value_index"
    index_mappings = {
        "dynamic": False,
        "properties": {
            "id": {"type": "keyword"}, # 用 keyword,因为它是精确标识
            "value": {
                "type": "text",  # 用 text,并配置 ik_max_word,因为它要参与中文全文检索
                "analyzer": "ik_max_word",
                "search_analyzer": "ik_max_word",
            },
            "column_id": {"type": "keyword"},    #  用 keyword,因为它也是精确标识;
        },
    }
    async def search(
    self,
    keyword: str,
    score_threshold: float = 0.6,
    limit: int = 20,
) -> list[ValueInfo]:
    resp = await self.client.search(
        index=self.index_name,
        query={
            # match 查询表示:用当前关键词去匹配 value_index 中的 value 字段。
            "match": {       
                "value": keyword,
            }
        },
        size=limit,
        min_score=score_threshold,
    )
    return [ValueInfo(**hit["_source"]) for hit in resp["hits"]["hits"]]

 完整链路:

用户输入 query
  -> extract_keywords
      -> jieba 抽关键词
      -> 加入原始 query
      -> 写入 state["keywords"]

  -> recall_column
      -> LLM 扩展字段召回关键词
      -> Embedding
      -> Qdrant 检索 ColumnInfo
      -> 写入 state["retrieved_column_infos"]

  -> recall_metric
      -> LLM 扩展指标召回关键词
      -> Embedding
      -> Qdrant 检索 MetricInfo
      -> 写入 state["retrieved_metric_infos"]

  -> recall_value
      -> LLM 扩展字段取值关键词
      -> Elasticsearch 检索 ValueInfo
      -> 写入 state["retrieved_value_infos"]

可以把整章压缩成下面这条链路:

query
  -> jieba 抽取关键词
  -> 保留原始 query 兜底
  -> LLM 分别扩展字段、指标、字段取值召回关键词
  -> 字段关键词 Embedding 后查 Qdrant,得到 ColumnInfo
  -> 指标关键词 Embedding 后查 Qdrant,得到 MetricInfo
  -> 字段取值关键词查 Elasticsearch,得到 ValueInfo
  -> 三路结果写回 DataAgentState

 

posted @ 2026-05-25 16:45  幻影之舞  阅读(19)  评论(0)    收藏  举报