告别上下文爆炸:基于 Milvus-Lite 与渐进式披露的动态 Agent 工具挂载实践

引言

此文章适合Agent应用开发者 阅读。适用场景:一个大型智能体应用/特定业务场景中,当Tool 数量激增 时,在挂载进prompt时,如何更好的去处理它(不占用过多的上下文窗口 )。这里,我采用的是渐进式披露 和本地向量库建索引的方式来解决这个问题

问题

实际开发中,对于我们的工具挂载,在多数情况下,都是直接一股脑把工具的作用、何时调用、工具的输入、输出等,直接塞进prompt。 如下示例:

python 复制代码
model = ChatOpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    model="qwen-turbo",
    temperature=0,
    max_retries=0,
    timeout=(10.0, 30.0),
    disable_streaming=False,
    reasoning=None,
    stream_usage=False,
)


@tool
def web_search_text(query: str):
    """【联网文本搜索工具】用于检索全网最新的美食图文攻略、餐厅资讯、老饕评测及本地餐饮文化背景。

    【适用场景】
    1. 餐厅与档口推荐:寻找特定城市、街区或商圈的高口碑餐厅、老字号、米其林/黑珍珠榜单。
    2. 菜品与风味科普:了解地域特色菜系配料、正宗吃法、起源历史及口味特点(如辣度、酸甜、麻香)。
    3. 实用探店攻略:查询餐厅的实时营业状态、排队时长、人均预算、订座规则及避坑指南。
    4. 时效性信息验证:确认网红店铺是否停业搬迁、是否有当季限定新品等。

    【不适用场景】
    - 请勿用于搜索图片、短视频等多媒体资源(图片需求请调用 web_search_multiplex)。
    - 基础常识问题(如"西红柿炒蛋怎么做"、"川菜发源地是哪里")无需频繁检索,模型应优先利用内置知识。

    Args:
        query: 检索关键词。必须高度凝练,突出核心实体词(地域名 + 品类/店名 + 诉求词),严禁使用长句或口语化表达,长度控制在20字以内。
               正例:
               - "广州老西关竹升面老字号推荐"
               - "重庆观音桥九街地道老火锅人均"
               - "潮汕牛肉火锅吊龙匙柄部位吃法"
               反例(禁止):
               - "我想去广州吃面,麻烦你帮我找找有什么好吃的店铺"(过于冗长)
               - "好吃的火锅"(缺少地域或核心特征约束)
    """
    print("调用了美食文本搜索")

    result_text_li = ""

    data = {
        "intent": "web_search",
        "web_search": result_text_li
    }
    return json.dumps(data, ensure_ascii=False)


@tool
def web_search_multiplex(query: str):
    """【多模态图片检索工具】用于抓取互联网上高质量的美食实拍大图、餐厅内外装潢、招牌摆盘特写等多媒体视觉资产。

    【适用场景】
    1. 招牌菜品特写:当回复中提到了具体菜品名称,需展示其色泽、刀工、摆盘或成菜外观时调用。
    2. 店面环境与氛围:展示复古老字号门头、网红打卡点、景观位或市井排档实景。
    3. 制作工艺图解:展示烤鸭片皮、拉面拉伸、糖画勾勒等特色制作过程图。

    【不适用场景】
    - 请勿用于获取餐厅地址、价格或文字评价等信息(文字需求请调用 web_search_text)。
    - 严禁在尚未确定具体菜名或店名时盲目发起泛化搜索。

    【调用时机与依赖规则】
    - 严格依赖原则:通常在调用 web_search_text 获得具体推荐目标之后,或者用户已明确指定菜名/店铺时调用。
    - 精确匹配原则:每次搜索必须绑定具体菜名或商户名,避免泛化搜索导致图不对文。

    Args:
        query: 图片检索关键词。必须是具体的实体名称,并可附带视觉限定修饰词(如"实拍图"、"菜品特写"、"横截面"、"门头实景"),严禁泛指词,长度控制在20字以内。
               正例:
               - "顺德双皮奶碗装实拍特写"
               - "北京全聚德挂炉烤鸭切片摆盘图"
               - "西安回民街老米家大雨泡馍实物图"
               反例(禁止):
               - "好吃的广东甜品图片"(缺乏具体单品)
               - "漂亮的餐厅照片"(指向不明)
    """

    print("模拟搜索")
    result_li = "搜索结果"
    data = {
        "intent": "web_search",
        "web_search": result_li
    }
    return json.dumps(data, ensure_ascii=False)


tools = [web_search_text, web_search_multiplex]
tools_by_name = {tool.name: tool for tool in tools}
model_with_tools = model.bind_tools(tools)


# 模型节点(调工具)
def llm_call(state: dict):
    prompt = f"""
    你是一名专业、挑剔且极具品味的美食探店向导与美食专栏作家。你的目标是为用户提供准确详实、图文并茂、具有极高实操价值的美食攻略。

    【工具协同调用原则】
    1. 时序与依赖逻辑(先文后图):
       - 当用户提出宽泛的美食咨询(如"推荐顺德特色小吃")时,严禁直接调用图片搜索。
       - 必须先调用 `web_search_text` 检索获取具体的口碑名店、代表性菜品及背景攻略。
       - 选定 1~2 个最核心的代表菜品或商户后,再调用 `web_search_multiplex` 补充视觉图片,确保"图文高度咬合"。
    2. 精准检索约束:
       - `web_search_text` 的关键词必须剥离修饰语,聚焦在"城市/商圈 + 菜品/餐厅 + 属性"上。
       - `web_search_multiplex` 的关键词必须精确到"具体菜品名/餐厅名 + 视觉限定词",杜绝概念化模糊搜索。
    3. 自主知识与免搜判断:
       - 仅在涉及实时排队、最新价格、具体小众冷门老店或需要图片素材时调用工具。
       - 对于通用的口味描述、烹饪通识原理、日常寒暄,请直接结合已有知识作答,避免过度调用。

    【最终呈现要求】
    - 结构清晰,包含风味特征、推荐理由、人均价格及探店避坑 Tips。
    - 将检索到的图片资源自然地嵌入在对应菜品或餐厅的文字描述下方,避免突兀堆砌。
    """

    return {
        "messages": [
            model_with_tools.invoke(
                [
                    SystemMessage(
                        content=prompt
                    )
                ]
                + state["messages"]
            )
        ],
        "llm_calls": state.get("llm_calls", 0) + 1,
        "user_message_init": state.get("user_message_init", ""),
        "tool_results": state.get("tool_results", {}),
        "id": state.get("id", {}),
        "rag_results": {}
    }

这样做的话,会造成上下文窗口爆炸,大幅增加Token成本。但是,不挂载又不行。在Agent系统中,工具正是扮演着智能体的手脚,没有工具怎么干活呢!所以,引入下面的解决方案。

解决方案

  1. 提取工具的命名+作用(这里浓缩一下,一句话表达出该工具的作用)。

  2. 前置层:用Milvus-lite做本地工具向量库,将用户的问题 和工具的作用 进行相似度匹配,拿到Top3的工具(这里直接拿json_schema),然后注入Prompt。

  3. 兜底方案:同时把"元工具"注入到Prompt中,如果Vector匹配到的工具不合适。再到元工具里面去拿数据。

代码示例如下

python 复制代码
model = ChatOpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    model="qwen-turbo",
    temperature=0,
    max_retries=0,
    timeout=(10.0, 30.0),
    disable_streaming=False,
    reasoning=None,
    stream_usage=False,
)

# 以下工具描述我就略写了,请各位读者把它当成很多、很详细。
@tool
def web_search_text(query: str):
    """【联网文本搜索工具】检索全网最新的美食图文攻略、餐厅资讯、老饕评测及本地餐饮文化背景。
    Args:
        query: 检索关键词,需精简聚焦(地域名 + 品类/店名 + 属性词),如 '顺德早茶老字号推荐'。

    """
    print(f"-->执行文本检索:{query}")
    return json.dumps({"intent":"web_search","result":"文本检索内容数据"},ensure_ascii=False)


@tool
def web_search_multiplex(query: str):
    """【多模态图片检索工具】抓取互联网上高清的美食实拍大图、餐厅内外景、招牌菜特写。必须在明确具体菜名或店名后使用。

    Args:
        query: 图片检索关键词,必须为明确的菜品或店铺实体,如 '顺德双皮奶碗装实拍特写'。
    """
    print(f"--> 执行多模态图片检索: {query}")
    return json.dumps({"intent": "web_search_image", "result": "图片URL列表"}, ensure_ascii=False)


@tool
def book_restaurant_table(restaurant_name: str, people_count: int, time: str):
    """【餐厅在线预订工具】用于协助用户预订餐厅餐位。

    Args:
        restaurant_name: 餐厅全称。
        people_count: 就餐人数。
        time: 就餐时间,格式如 '2026-10-08 18:30'。
    """
    print(f"-> 执行订座: {restaurant_name}, {people_count}人, 时间: {time}")
    return json.dumps({"status": "success", "msg": "预订成功"}, ensure_ascii=False)

# 工具注册表
ALL_TOOLS: Dict[str,Any] = {
    "web_search_text": web_search_text,
    "web_search_multiplex": web_search_multiplex,
    "book_restaurant_table": book_restaurant_table,
}
# 浓缩描述(轻量级元信息)
TOOL_CONCISE_REGISTRY = [
    {
        "name":"web_search_text",
        "brief": "搜索美食文本攻略、特色菜介绍、餐厅榜单评价及口味特色"
    },
    {
        "name": "web_search_multiplex",
        "brief": "搜索美食菜品实拍大图、餐厅环境外观及摆盘照片"
    },
    {
        "name": "book_restaurant_table",
        "brief": "预订餐厅桌位、确认就餐人数及订座时间"
    }
]

# 前置向量层: Milvus-Lite本地工具检索

# 初始化本地Milvus-Lite单文件实例
milvus_client = MilvusClient(uri="./tools_milvus_lite.db")

COLLECTION_NAME = "tool_registry_concise"

def mock_embedding(text: str) -> List[float]:
    """生成稠密向量(模拟 Embedding,生产环境直接接入OpenAI等嵌入模型)"""
    return [1.2,2.21,2,45,1.8,5.9]

def init_vector_db():
    """初始化并写入浓缩工具索引"""
    if milvus_client.has_collection(COLLECTION_NAME):
        milvus_client.drop_collection(COLLECTION_NAME)

    milvus_client.create_collection(
        collection_name=COLLECTION_NAME,
        dimension = 128,
        metric_type = "COSINE"
    )

    data = []
    for idx,item in enumerate(TOOL_CONCISE_REGISTRY):
        text_to_embed = f"{item['name']}:{item['brief']}"
        data.append({
            "id":idx,
            "vector":mock_embedding(text_to_embed),
            "tool_name":item["name"],
            "brief": item["brief"]
        })
    milvus_client.insert(collection_name=COLLECTION_NAME,data=data)

# 初始化本地工具库
init_vector_db()

def search_top_k_tools(query:str,top_k:int=3) -> List[str]:
    """前置检索:通过用户Query召回Top-K工具名"""
    q_vec = mock_embedding(query)
    res = milvus_client.search(
        collection_name=COLLECTION_NAME,
        data=[q_vec],
        limit=top_k,
        output_fields=["tool_name"]
    )
    selected_names = [hit["entity"]["tool_name"] for hit in res[0]]
    return selected_names

# 这里充当查询重写的作用
@tool
def search_and_load_tools(query: str):
    """【工具库动态检索加载器】
    当当前绑定的工具无法满足用户任务(如缺少订座、外卖、路线导航等功能)时调用。

    Args:
        query: 工具能力的抽象检索关键词(必须由2-3个同义的功能/API动词组成,用空格分隔,严禁输入长句或包含时间、地点等具体参数)。
               正例:"外卖送餐 跑腿代购"、"路线导航 公交地铁"、"桌位预订 餐厅排号"
               反例:"顺德有什么好吃的"、"预订今晚7点的4人桌"
    """
    found_names = search_top_k_tools(query,top_k=2)
    return json.dumps({"loaded_tools":found_names},ensure_ascii=False)

def execute_agent_step(user_query: str,max_steps: int=3):
    """
    动态渐进式披露主逻辑:
    1. 前置层:向量检索 Top3 目标工具
    2. 兜底层:注入元工具 load_tools
    3. 动态绑定工具集并调用 LLM
    """

    # 1. 向量相似度检索Top-K工具
    top_tool_names = search_top_k_tools(user_query,top_k=3)

    active_tools_map = {name: ALL_TOOLS[name] for name in top_tool_names if name in ALL_TOOLS}
    active_tools_map["search_and_load_tools"] = search_and_load_tools
    # 这里的prompt简写了,将就看。
    messages = [
        SystemMessage("你是一名美食向导。若缺少工具,请调用 search_and_load_tools。"),
        HumanMessage(content=user_query)
    ]
     # 一个ReAct
    for _ in range(max_steps):
        # 动态绑定当前可用工具
        current_model = model.bind_tools(list(active_tools_map.values()))
        response = current_model.invoke(messages)
        messages.append(response)

        # 检查是否有tool_calls
        if not response.tool_calls:
            return response.content

        # 处理工具调用
        for tool_call in response.tool_calls:
            tool_name = tool_call["name"]
            tool_args = tool_call["args"]

            if tool_name == "search_and_load_tools":
                # 执行加载元工具
                result_str = search_and_load_tools.invoke(tool_args)
                loaded_names = json.loads(result_str).get("loaded_tools",[])

                for name in loaded_names:
                    if name in ALL_TOOLS:
                        active_tools_map[name] = ALL_TOOLS[name]

                # 回传 ToolMessage告知模型工具已加载
                messages.append(ToolMessage(
                    content=f"已成功加载新工具: {loaded_names},现在可以直接调用它们。",
                    tool_call_id = tool_call["id"]
                ))
            else:
                # 执行具体业务工具
                tool_func = active_tools_map.get(tool_name)
                result_str = tool_func.invoke(tool_args) if tool_func else "未找到工具"
                messages.append(ToolMessage(
                    content=str(result_str),
                    tool_call_id = tool_call["id"]
                ))
    return messages[-1].content

本地运行 Milvus Lite | Milvus 文档

LangChain中文网 - 跟着LangChain学AI开发

总结

通过上述示例,通过使用渐进式披露和本地向量库的方式,解决了在工具多而杂的复杂场景下,上下文窗口膨胀的问题。

文章可能存在一些没考虑到的问题,请各位前辈轻点。

相关推荐
炸鸡叔1 小时前
我做了 BotBus:从手机续聊本地 Agent,查看文件和终端
前端·后端
Gust of wind1 小时前
串与KMP模式匹配:存储结构、基本操作、next数组手算
c语言·数据结构·后端·算法
endswel2 小时前
Spring bean 注册多种方式
java·后端·spring
铁皮饭盒2 小时前
还是网页端, 46mb模型, 抠图功能升级了, 抠任意主体, 还是不要显卡, 不要python, 满意吗?
前端·javascript·后端
小林coding2 小时前
Flash模型又添一员大将:实测MiniMax M3.1
后端
颜进强2 小时前
25 · NestJs DurableProviders 持久化 Provider:把 ContextId 当缓存键
前端·后端·ai编程
tltwuyulw3 小时前
Java的函数式编程(四)
后端
我的xiaodoujiao3 小时前
Django 基础知识详细图文教程 14-Django 表单定义与使用
开发语言·后端·python·django