用 LangChain 构建电影 RAG Agent:向量检索、重排序与 Human-in-the-loop
用 LangChain 构建电影 RAG Agent:向量检索、重排序与 Human-in-the-loop
本文基于项目中的
rag-demo.py,介绍如何用 LangChain v1 + LangGraph 搭建一个「查电影 → 提取关键词 → 人工审批后写入」的 RAG Agent。完整源码见rag-demo.py,流程速查见rag-demo-flow.md。
背景:我们到底要解决什么问题?
想象这样一个场景:用户输入「请查询电影《Kampfansage》,提取关键词并保存」。Agent 需要:
- 从上千部电影里找到正确的那一部(可能片名不完整、还是德语);
- 读懂剧情后生成 5 个英文关键词;
- 把关键词写回向量库和本地 JSON,供后续检索使用。
第 3 步是有副作用的写操作——如果 LLM 幻觉出错误关键词,会直接污染数据。因此我们在 demo 里加了一层 Human-in-the-loop(HITL):检索自动执行,保存前必须人工点批准。
整体架构如下:
技术选型
| 层次 | 选型 | 理由 |
|---|---|---|
| 数据源 | MongoDB/embedded_movies | 自带电影 metadata,适合 RAG demo |
| 嵌入 | BAAI/bge-m3 |
多语言、长文本,德语/日语片名比 MiniLM 稳 |
| 重排序 | BAAI/bge-reranker-v2-m3 |
向量召回 Top-K 后再精排,减少误匹配 |
| 向量库 | Chroma | 轻量、本地持久化 |
| Agent | create_agent + LangGraph |
工具调用 + checkpoint + middleware |
| HITL | HumanInTheLoopMiddleware |
写工具执行前 interrupt,人工 resume |
第一步:准备数据,统一 ID
数据从 HuggingFace 下载,去掉无剧情的记录,并为每条电影生成稳定 id。这个 id 会同时出现在 JSON 源文件和 Chroma 里,后续更新关键词靠它对齐。
def download_dataset():
dataset = hf_load_dataset("MongoDB/embedded_movies")
dataset_df = pd.DataFrame(dataset["train"])
dataset_df = dataset_df.dropna(subset=["plot"])
dataset_df = dataset_df.drop(columns=["plot_embedding"])
records = dataset_df.to_dict(orient="records")
for record in records:
if not record.get("id"):
record["id"] = uuid.uuid4().hex
with open(DATASET_PATH, "w", encoding="utf-8") as f:
for record in records:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
每条电影被转成 LangChain Document,page_content 里拼上标题、类型、导演、演员、剧情等字段——embedding 的是整段文本,而不只是标题:
def _record_to_document(record: dict) -> Document:
title = record.get("title", "Unknown")
plot = record.get("fullplot") or record.get("plot", "")
genres = ", ".join(record.get("genres") or [])
# ...
page_content = (
f"Title: {title}\n"
f"Genres: {genres}\n"
f"Directors: {directors}\n"
f"Cast: {cast}\n"
f"Plot: {plot}\n"
# ...
)
return Document(
page_content=page_content,
metadata={"title": title, "id": record.get("id", ""), ...},
)
然后批量嵌入并写入 Chroma:
EMBEDDING_MODEL = "BAAI/bge-m3"
def embed_dataset(rebuild: bool = True):
if rebuild and CHROMA_DIR.exists():
shutil.rmtree(CHROMA_DIR)
documents, ids = [], []
for record in load_dataset():
if record.get("plot") or record.get("fullplot"):
documents.append(_record_to_document(record))
ids.append(record.get("id"))
return Chroma.from_documents(
documents=documents,
embedding=HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL),
ids=ids,
persist_directory=str(CHROMA_DIR),
collection_name="movies",
)
踩坑提示:换嵌入模型后必须
rebuild=True重建向量库。MiniLM(384 维)和 bge-m3(1024 维)不能混用。首次下载 bge-m3 约 2GB,国内网络建议设置HF_ENDPOINT=https://hf-mirror.com。
第二步:两阶段检索——向量召回 + Cross-Encoder 重排
单纯 similarity_search(query, k=1) 容易翻车。例如输入 Kampfansage,向量库里的完整片名是 Kampfansage - Der letzte Schèler,精确 metadata 匹配失败;而英文 embedding 可能把 Kamikaze Taxi(剧情含 kamikaze)排到第一。
当前 demo 的检索策略是:
- Chroma metadata 精确匹配
title - 向量召回 Top-15 + bge-reranker 精排取 Top-1
RERANKER_MODEL = "BAAI/bge-reranker-v2-m3"
def find_movie_by_title(vectorstore: Chroma, title: str) -> Document | None:
result = vectorstore.get(where={"title": title}, limit=1)
if result["ids"]:
return Document(
page_content=result["documents"][0],
metadata=result["metadatas"][0] or {},
id=result["ids"][0],
)
candidates = vectorstore.similarity_search(title, k=15)
if not candidates:
return None
reranker = CrossEncoderReranker(
model=HuggingFaceCrossEncoder(model_name=RERANKER_MODEL),
top_n=1,
)
return reranker.compress_documents(candidates, query=title)[0]
代码里还保留了 find_record_by_title_fuzzy()(子串匹配标题),目前被注释。对「只输入部分片名」的场景,模糊标题匹配往往比纯向量更可靠,生产环境建议重新启用作为 step 2,向量检索作为兜底。
第三步:把检索封装成 Agent 工具
Agent 不直接碰 Chroma,而是通过 @tool 暴露能力:
@tool
def search_movie(title: str) -> str:
"""从向量库 RAG 检索电影。传入电影名,返回剧情、演员、类型、doc_id 等信息。"""
doc = find_movie_by_title(get_vectorstore(), title)
if doc is None:
return f"未找到电影: {title}"
return (
f"title: {doc.metadata.get('title')}\n"
f"doc_id: {doc.id}\n"
f"genres: {doc.metadata.get('genres', '')}\n"
f"keywords: {doc.metadata.get('keywords') or '无'}\n"
f"content:\n{doc.page_content}"
)
@tool
def save_movie_keywords(record_id: str, keywords: str) -> str:
"""将关键词保存到向量库,并同步到 dataset 源文件。"""
vectorstore = get_vectorstore()
_update_movie_keywords(vectorstore, record_id, keywords)
_sync_keywords_to_dataset(record_id, keywords)
# ...
save_movie_keywords 做两件事:
_update_movie_keywords:改 Chroma 文档内容并重新 embedding_sync_keywords_to_dataset:写回dataset.textJSONL
双写保证向量库与源文件一致。
第四步:Human-in-the-loop——写操作必须人工点头
LangChain 的 HumanInTheLoopMiddleware 会在 Agent 发出工具调用后、真正执行前 interrupt。要 resume,必须:
- 配置 checkpointer(这里用
InMemorySaver) - 每次 invoke 使用相同的
thread_id - 人工决策后,用
Command(resume={"decisions": [...]})继续
Agent 创建:
_checkpointer = InMemorySaver()
def create_movie_rag_agent():
return create_agent(
model=chat_llm(),
system_prompt=SYSTEM_PROMPT,
tools=[search_movie, save_movie_keywords],
checkpointer=_checkpointer,
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"search_movie": False, # 只读,自动执行
"save_movie_keywords": { # 写操作,需审批
"allowed_decisions": ["approve", "edit", "reject"],
},
},
description_prefix="保存关键词前需人工确认",
),
],
)
| 工具 | interrupt | 说明 |
|---|---|---|
search_movie |
False |
查询无副作用,不打扰用户 |
save_movie_keywords |
approve / edit / reject |
写库前必须人工确认 |
invoke → interrupt → resume 循环
def invoke_with_hitl(agent, payload, config: dict):
result = agent.invoke(payload, config=config, version="v2")
while result.interrupts:
interrupt_value = result.interrupts[0].value
action_requests = interrupt_value["action_requests"]
# 展示待审批的工具和参数,收集人工决策
_print_hitl_request(interrupt_value)
decisions = [
_prompt_hitl_decision(action, ...)
for action in action_requests
]
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config,
version="v2",
)
return result
三种决策的含义:
- approve:按 LLM 原参数执行工具
- edit:人工修改
record_id/keywords后再执行 - reject:不执行,把拒绝原因反馈给 LLM,由 Agent 重新组织回复
终端交互示例:
--- 待审批工具调用 ---
[1] 工具: save_movie_keywords
参数: {"record_id": "8d417880...", "keywords": "Action, Revenge, ..."}
审批 `save_movie_keywords`
人工决策 (approve/edit/reject): e
请输入修改后的 args(JSON):
> {"record_id": "8d417880...", "keywords": "Post-apocalyptic, Martial arts, Survival"}
第五步:跑起来
.env 中配置 LLM:
base_url=https://your-api-endpoint/v1
api_key=sk-xxx
model=your-model-name
离线建库(首次或换模型后):
download_dataset()
embed_dataset(rebuild=True)
在线交互(默认入口):
python rag-demo.py
程序进入 run_interactive_hitl(),多轮对话共享同一 thread_id,写操作每次都会弹出审批。
System Prompt 约束 Agent 的行为顺序:
1. 先 search_movie
2. 提取 5 个英文关键词
3. 再 save_movie_keywords
4. 向用户说明结果
一次完整对话里发生了什么?
用户输入:
请查询电影《Kampfansage》,提取关键词并保存
| 阶段 | 发生了什么 |
|---|---|
| 1 | LLM 调用 search_movie("Kampfansage") → 自动执行,无 interrupt |
| 2 | 检索模块走向量召回 + rerank,返回电影详情和 doc_id |
| 3 | LLM 根据剧情生成关键词,调用 save_movie_keywords(...) |
| 4 | HumanInTheLoopMiddleware 触发 interrupt,终端等待人工输入 |
| 5 | 用户 approve / edit / reject |
| 6 | Command(resume=...) 恢复执行;若 approve,双写 Chroma + JSON |
| 7 | LLM 生成最终自然语言回复 |
设计取舍与可改进点
已做的
- 多语言嵌入 + rerank,比单用 MiniLM 稳
- 读/写工具分离,HITL 只拦写操作
- JSONL 与 Chroma 共用 id,更新路径清晰
可改进
- 重新启用模糊标题匹配:部分片名输入时比纯向量更准
- 嵌入模型懒加载 + 下载进度:bge-m3 首次加载易误以为卡死
- HITL 接入 Web UI:终端
input()适合 demo,生产需审批台 RETRIEVE_K与k=15统一:常量已定义但未完全使用
总结
rag-demo.py 串联了 RAG 管线里几个常见环节:离线建库 → 向量召回 → 重排序 → Agent 工具化 → 写操作人工审批。它不是一个「聊天查电影」的简单 demo,而是一个可扩展的骨架——你可以把 save_movie_keywords 换成「写入 CMS」「发邮件通知」等任何敏感工具,同样用 HumanInTheLoopMiddleware 挂一层安全阀。
核心链路一句话概括:
用户说话 → Agent RAG 查电影 → LLM 提议保存 → 人工审批 → 双写向量库与 JSON → Agent 回复。
完整代码
import uuid
from datasets import load_dataset as hf_load_dataset
import pandas as pd
import json
import shutil
from pathlib import Path
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langchain.tools import tool
from langchain_chroma import Chroma
from langchain_classic.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
from langchain_core.documents import Document
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_openai import ChatOpenAI
import dotenv
import os
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
dotenv.load_dotenv()
# https://huggingface.co/datasets/AIatMongoDB/embedded\_movies
DATASET_PATH = Path("dataset.text")
CHROMA_DIR = Path("./chroma_db")
EMBEDDING_MODEL = "BAAI/bge-m3"
RERANKER_MODEL = "BAAI/bge-reranker-v2-m3"
RETRIEVE_K = 20
COLLECTION_NAME = "movies"
def get_embeddings() -> HuggingFaceEmbeddings:
return HuggingFaceEmbeddings(model_name=EMBEDDING_MODEL)
def get_reranker() -> CrossEncoderReranker:
cross_encoder = HuggingFaceCrossEncoder(model_name=RERANKER_MODEL)
return CrossEncoderReranker(model=cross_encoder, top_n=1)
def download_dataset():
dataset = hf_load_dataset("MongoDB/embedded_movies")
dataset_df = pd.DataFrame(dataset['train'])
dataset_df = dataset_df.dropna(subset=['plot'])
print("\nNumber of missing values in each column after removal:")
print(dataset_df.isnull().sum())
dataset_df = dataset_df.drop(columns=['plot_embedding'])
# 写入源文件时生成稳定 id,后续向量库与 JSON 共用同一 id
records = dataset_df.to_dict(orient="records")
for record in records:
if not record.get("id"):
record["id"] = uuid.uuid4().hex
with open(DATASET_PATH, 'w', encoding='utf-8') as f:
for record in records:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
def load_dataset():
"""从 JSONL 文件加载电影数据(每行一条 JSON 记录)。"""
data = []
with open(DATASET_PATH, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if line:
data.append(json.loads(line))
return data
def find_record_by_id(record_id: str) -> dict | None:
"""根据 id 从源数据中查找记录。"""
for record in load_dataset():
if record.get("id") == record_id:
return record
return None
def find_record_by_title_fuzzy(title: str) -> dict | None:
"""按标题模糊匹配:支持用户只输入片名的一部分(如 Kampfansage)。"""
query = title.strip().lower()
if not query:
return None
candidates: list[dict] = []
for record in load_dataset():
record_title = (record.get("title") or "").strip()
if not record_title:
continue
record_lower = record_title.lower()
if query == record_lower:
return record
if query in record_lower or record_lower in query:
candidates.append(record)
if not candidates:
return None
# 多条命中时取标题最短的一条,通常与用户输入最接近
return min(candidates, key=lambda r: len(r.get("title") or ""))
def _record_to_document(record: dict) -> Document:
"""将单条电影记录转为 LangChain Document。"""
title = record.get("title", "Unknown")
plot = record.get("fullplot") or record.get("plot", "")
genres = ", ".join(record.get("genres") or [])
cast = ", ".join(record.get("cast") or [])
directors = ", ".join(record.get("directors") or [])
writers = ", ".join(record.get("writers") or [])
languages = ", ".join(record.get("languages") or [])
keywords = record.get("keywords") or []
if isinstance(keywords, list):
keywords = ", ".join(keywords)
page_content = (
f"Title: {title}\n"
f"Genres: {genres}\n"
f"Directors: {directors}\n"
f"Cast: {cast}\n"
f"Plot: {plot}\n"
f"Writers: {writers}\n"
f"Languages: {languages}\n"
)
if keywords:
page_content += f"Keywords: {keywords}\n"
return Document(
page_content=page_content,
metadata={
"title": title,
"type": record.get("type", ""),
"genres": genres,
"keywords": keywords,
"id": record.get("id", "")
},
)
def embed_dataset(persist_directory: str | Path = CHROMA_DIR, rebuild: bool = True):
"""读取 dataset,生成 embedding 并写入 Chroma 向量库。"""
persist_directory = Path(persist_directory)
if rebuild and persist_directory.exists():
shutil.rmtree(persist_directory)
embeddings = get_embeddings()
data = load_dataset()
documents = []
ids = []
for index, record in enumerate(data):
if not (record.get("plot") or record.get("fullplot")):
continue
documents.append(_record_to_document(record))
ids.append(record.get("id"))
print(f"Loaded {len(documents)} documents from {DATASET_PATH}")
vectorstore = Chroma.from_documents(
documents=documents,
embedding=embeddings,
ids=ids,
persist_directory=str(persist_directory),
collection_name=COLLECTION_NAME,
)
print(f"Embeddings saved to {persist_directory}")
return vectorstore
def load_vectorstore(persist_directory: str | Path = CHROMA_DIR) -> Chroma:
"""加载已持久化的 Chroma 向量库。"""
return Chroma(
persist_directory=str(persist_directory),
embedding_function=get_embeddings(),
collection_name=COLLECTION_NAME,
)
def find_movie_by_title(vectorstore: Chroma, title: str) -> Document | None:
"""按标题查找电影:metadata 精确匹配 → 源数据模糊标题 → 向量相似度。"""
title = title.strip()
if not title:
return None
result = vectorstore.get(where={"title": title}, limit=1)
print("step1 ", result)
if result["ids"]:
return Document(
page_content=result["documents"][0],
metadata=result["metadatas"][0] or {},
id=result["ids"][0],
)
# fuzzy_record = find_record_by_title_fuzzy(title)
# if fuzzy_record and fuzzy_record.get("id"):
# docs = vectorstore.get_by_ids([fuzzy_record["id"]])
# if docs:
# doc = docs[0]
# doc.id = fuzzy_record["id"]
# return doc
candidates = vectorstore.similarity_search(title, k=15)
print("step2 ", candidates)
if not candidates:
return None
return get_reranker().compress_documents(candidates, query=title)[0]
def _update_movie_keywords(vectorstore: Chroma, doc_id: str, keywords: str) -> Document:
"""更新向量库中电影的关键词,并重新计算 embedding。"""
docs = vectorstore.get_by_ids([doc_id])
if not docs:
raise ValueError(f"Document not found: {doc_id}")
doc = docs[0]
base_content = doc.page_content.split("\nKeywords:")[0].rstrip()
updated_doc = Document(
page_content=f"{base_content}\nKeywords: {keywords}\n",
metadata={**doc.metadata, "keywords": keywords},
)
vectorstore.update_document(doc_id, updated_doc)
updated_doc.id = doc_id
return updated_doc
def _sync_keywords_to_dataset(record_id: str, keywords: str) -> None:
"""根据 id 将关键词同步写回 JSON 源文件。"""
records = load_dataset()
updated = False
for record in records:
if record.get("id") == record_id:
record["keywords"] = [k.strip() for k in keywords.split(",") if k.strip()]
updated = True
break
if not updated:
return
with open(DATASET_PATH, "w", encoding="utf-8") as f:
for record in records:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
_vectorstore: Chroma | None = None
_checkpointer = InMemorySaver()
def get_vectorstore() -> Chroma:
global _vectorstore
if _vectorstore is None:
_vectorstore = load_vectorstore()
return _vectorstore
@tool
def search_movie(title: str) -> str:
"""从向量库 RAG 检索电影。传入电影名,返回剧情、演员、类型、doc_id 等信息。"""
doc = find_movie_by_title(get_vectorstore(), title)
if doc is None:
return f"未找到电影: {title}"
return (
f"title: {doc.metadata.get('title')}\n"
f"doc_id: {doc.id}\n"
f"genres: {doc.metadata.get('genres', '')}\n"
f"keywords: {doc.metadata.get('keywords') or '无'}\n"
f"content:\n{doc.page_content}"
)
@tool
def save_movie_keywords(record_id: str, keywords: str) -> str:
"""将关键词保存到向量库,并同步到 dataset 源文件。record_id 来自 search_movie 返回的 doc_id。"""
vectorstore = get_vectorstore()
_update_movie_keywords(vectorstore, record_id, keywords)
_sync_keywords_to_dataset(record_id, keywords)
record = find_record_by_id(record_id)
title = record.get("title") if record else record_id
return f"已保存《{title}》的关键词: {keywords}"
SYSTEM_PROMPT = """你是专业的电影评论员,可以使用以下工具:
- search_movie: 从向量库 RAG 检索电影详情(必须先调用)
- save_movie_keywords: 保存关键词(需要 search_movie 返回的 doc_id)
工作流程:
1. 用户给出电影名时,先调用 search_movie 查询
2. 根据检索内容提取 5 个英文关键词
3. 调用 save_movie_keywords(record_id=doc_id, keywords=...) 保存
4. 向用户说明电影信息和已保存的关键词
"""
def chat_llm() -> ChatOpenAI:
"""与 LLM 进行对话。"""
llm = ChatOpenAI(
base_url=os.getenv("base_url"),
api_key=os.getenv("api_key"),
model=os.getenv("model"),
)
return llm
def create_movie_rag_agent():
"""创建带 Human-in-the-loop 的电影 RAG Agent(写操作需人工审批)。"""
return create_agent(
model=chat_llm(),
system_prompt=SYSTEM_PROMPT,
tools=[search_movie, save_movie_keywords],
checkpointer=_checkpointer,
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"search_movie": False,
"save_movie_keywords": {
"allowed_decisions": ["approve", "edit", "reject"],
},
},
description_prefix="保存关键词前需人工确认",
),
],
)
def _print_hitl_request(interrupt_value: dict) -> None:
action_requests = interrupt_value["action_requests"]
review_configs = interrupt_value["review_configs"]
config_map = {cfg["action_name"]: cfg for cfg in review_configs}
print("----------------------")
print("interrupt_value ",json.dumps(interrupt_value, ensure_ascii=False))
print("\n--- 待审批工具调用 ---")
for index, action in enumerate(action_requests, start=1):
review = config_map[action["name"]]
print(f"[{index}] 工具: {action['name']}")
print(f" 参数: {json.dumps(action['args'], ensure_ascii=False)}")
if action.get("description"):
print(f" 说明: {action['description']}")
print(f" 可选: {review['allowed_decisions']}")
print("----------------------")
def _prompt_hitl_decision(action: dict, allowed_decisions: list[str]) -> dict:
print(f"\n审批 `{action['name']}`")
print(json.dumps(action["args"], ensure_ascii=False, indent=2))
while True:
hint = "/".join(allowed_decisions)
choice = input(f"人工决策 ({hint}): ").strip().lower()
if choice in {"approve", "a", "y", "yes"} and "approve" in allowed_decisions:
return {"type": "approve"}
if choice in {"reject", "r", "n", "no"} and "reject" in allowed_decisions:
message = input("拒绝原因(回车跳过): ").strip()
decision: dict = {"type": "reject"}
if message:
decision["message"] = message
return decision
if choice in {"edit", "e"} and "edit" in allowed_decisions:
print("请输入修改后的 args(JSON),例如:")
print(' {"record_id": "xxx", "keywords": "Action, Drama, ..."}')
raw = input("> ").strip()
try:
args = json.loads(raw)
if "name" in args and "args" in args:
return {"type": "edit", "edited_action": args}
return {
"type": "edit",
"edited_action": {"name": action["name"], "args": args},
}
except json.JSONDecodeError as err:
print(f"JSON 无效: {err}")
print("无效输入,请重试。")
def invoke_with_hitl(agent, payload, config: dict):
"""执行 Agent,遇到 interrupt 时暂停并等待人工审批后 resume。"""
result = agent.invoke(payload, config=config, version="v2")
while result.interrupts:
interrupt_value = result.interrupts[0].value
action_requests = interrupt_value["action_requests"]
review_configs = interrupt_value["review_configs"]
config_map = {cfg["action_name"]: cfg for cfg in review_configs}
_print_hitl_request(interrupt_value)
decisions = [
_prompt_hitl_decision(action, config_map[action["name"]]["allowed_decisions"])
for action in action_requests
]
result = agent.invoke(
Command(resume={"decisions": decisions}),
config=config,
version="v2",
)
return result
def _final_agent_text(result) -> str:
value = result.value if hasattr(result, "value") else result
messages = value.get("messages", [])
if not messages:
return ""
return messages[-1].content
def run_interactive_hitl(thread_id: str = "movie-rag-hitl") -> None:
"""交互式对话 + Human-in-the-loop:save_movie_keywords 执行前需人工审批。"""
agent = create_movie_rag_agent()
config = {"configurable": {"thread_id": thread_id}}
print("电影 RAG 助手(Human-in-the-loop)")
print("- search_movie 自动执行")
print("- save_movie_keywords 需人工 approve / edit / reject")
print("- 输入 quit / 退出 结束\n")
while True:
try:
user_input = input("你: ").strip()
except (EOFError, KeyboardInterrupt):
print("\n再见。")
break
if not user_input:
continue
if user_input.lower() in {"quit", "exit", "q", "退出", "bye"}:
print("再见。")
break
try:
result = invoke_with_hitl(
agent,
{"messages": [{"role": "user", "content": user_input}]},
config,
)
print(f"助手: {_final_agent_text(result)}\n")
except Exception as e:
print(f"错误: {e}\n")
def execute_agent(movie_title: str):
agent = create_movie_rag_agent()
config = {"configurable": {"thread_id": "movie-rag-1"}}
result = invoke_with_hitl(
agent,
{
"messages": [
{
"role": "user",
"content": f"请查询电影《{movie_title}》,提取关键词并保存",
}
]
},
config,
)
print(_final_agent_text(result))
def verify(movie_title:str):
vectorstore = get_vectorstore()
doc = find_movie_by_title(vectorstore, movie_title)
print(doc.metadata)
if __name__ == "__main__":
run_interactive_hitl()

浙公网安备 33010602011771号