引言
此文章适合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系统中,工具正是扮演着智能体的手脚,没有工具怎么干活呢!所以,引入下面的解决方案。
解决方案
-
提取工具的命名+作用(这里浓缩一下,一句话表达出该工具的作用)。
-
前置层:用Milvus-lite做本地工具向量库,将用户的问题 和工具的作用 进行相似度匹配,拿到Top3的工具(这里直接拿json_schema),然后注入Prompt。
-
兜底方案:同时把"元工具"注入到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
LangChain中文网 - 跟着LangChain学AI开发
总结
通过上述示例,通过使用渐进式披露和本地向量库的方式,解决了在工具多而杂的复杂场景下,上下文窗口膨胀的问题。
文章可能存在一些没考虑到的问题,请各位前辈轻点。