* Agentic RAG * Fix formatting * Agent supervisor * fix links * SQL agent * Graph Runs in LS * fix format * Autogen + LG tutorial * fixes * update sql * remove old notebooks * docs: Add section about data region for LGP data plane (#5378) Add section about data region. * fix: remove empty notebook (#5379) * Fix docstring for _unset_config_context function (#5374) Signed-off-by: jitokim <pigberger70@gmail.com> * Fix typo in StreamMode debug description: checlkpoints → checkpoints (#5371) Signed-off-by: jitokim <pigberger70@gmail.com> * fix: remove unused import in generate_llms_text.py (#5380) * dcos: Fix deprecation of TavilySearch (#5375) Fix deprecation: The class `TavilySearchResults` was deprecated in LangChain 0.3.25 and will be removed in 1.0 * Fix typo: funtion → function (#5370) fix typos Signed-off-by: jitokim <pigberger70@gmail.com> * docs: feedback edits (#5387) * docs: update lgp deployment metric list (#5388) * chore(deps): bump peter-evans/create-pull-request from 6 to 7 (#5365) Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6 to 7. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v6...v7) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs: correct link in docs/docs/how-tos/graph-api.md (#5377) Update graph-api.md * docs: Update quick_start.md Rest API Guide (#5368) Update quick_start.md Rest API Guide The curl command in the quick start needs some minor changes to work out of the box. I hope by adding these changes then new users can get started more quickly * Remove duplicate CONFIG_KEY_CHECKPOINT_MAP from RESERVED set (#5372) Signed-off-by: jitokim <pigberger70@gmail.com> * docs: fix typo in persistence (#5329) * docs: fix typo in application_structure * docs: fix typo in persistence * chore[deps]: upgrade dependencies with `uv lock --upgrade` (#5358) * chore: upgrade dependencies with `uv lock --upgrade` * linting * upgrade PR title --------- Co-authored-by: sydney-runkle <54324534+sydney-runkle@users.noreply.github.com> Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com> * Updated examples for SummarizationNode to account for serde with persistence layers (#5257) * docs: move script into scripts (#5384) * docs: update sql tutorial (#5389) --------- Signed-off-by: jitokim <pigberger70@gmail.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Andrew Nguonly <andrewnguonly@users.noreply.github.com> Co-authored-by: Michael Li <michaelli65535@gmail.com> Co-authored-by: jito <pigberger70@gmail.com> Co-authored-by: Serhii Polishchuk <serhii.polishchuk@gelato.com> Co-authored-by: hari-dhanushkodi <hari@langchain.dev> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Fadel Akram <af8356207@gmail.com> Co-authored-by: David <31293924+dreadn0ught@users.noreply.github.com> Co-authored-by: Youssef Ahmed Mohamed Abdelrahman <109446360+unauthorised-401@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: sydney-runkle <54324534+sydney-runkle@users.noreply.github.com> Co-authored-by: Sydney Runkle <sydneymarierunkle@gmail.com> Co-authored-by: Nick Riley <nick@sparkida.com> Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com> Co-authored-by: ccurme <chester.curme@gmail.com>
18 KiB
Agentic RAG
In this tutorial we will build a retrieval agent. Retrieval agents are useful when you want an LLM to make a decision about whether to retrieve context from a vectorstore or respond to the user directly.
By the end of the tutorial we will have done the following:
- Fetch and preprocess documents that will be used for retrieval.
- Index those documents for semantic search and create a retriever tool for the agent.
- Build an agentic RAG system that can decide when to use the retriever tool.
Setup
Let's download the required packages and set our API keys:
%%capture --no-stderr
%pip install -U --quiet langgraph "langchain[openai]" langchain-community langchain-text-splitters
import getpass
import os
def _set_env(key: str):
if key not in os.environ:
os.environ[key] = getpass.getpass(f"{key}:")
_set_env("OPENAI_API_KEY")
!!! tip Sign up for LangSmith to quickly spot issues and improve the performance of your LangGraph projects. LangSmith lets you use trace data to debug, test, and monitor your LLM apps built with LangGraph.
1. Preprocess documents
-
Fetch documents to use in our RAG system. We will use three of the most recent pages from Lilian Weng's excellent blog. We'll start by fetching the content of the pages using
WebBaseLoaderutility:from langchain_community.document_loaders import WebBaseLoader urls = [ "https://lilianweng.github.io/posts/2024-11-28-reward-hacking/", "https://lilianweng.github.io/posts/2024-07-07-hallucination/", "https://lilianweng.github.io/posts/2024-04-12-diffusion-video/", ] docs = [WebBaseLoader(url).load() for url in urls]docs[0][0].page_content.strip()[:1000] -
Split the fetched documents into smaller chunks for indexing into our vectorstore:
from langchain_text_splitters import RecursiveCharacterTextSplitter docs_list = [item for sublist in docs for item in sublist] text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( chunk_size=100, chunk_overlap=50 ) doc_splits = text_splitter.split_documents(docs_list)doc_splits[0].page_content.strip()
2. Create a retriever tool
Now that we have our split documents, we can index them into a vector store that we'll use for semantic search.
-
Use an in-memory vector store and OpenAI embeddings:
from langchain_core.vectorstores import InMemoryVectorStore from langchain_openai import OpenAIEmbeddings vectorstore = InMemoryVectorStore.from_documents( documents=doc_splits, embedding=OpenAIEmbeddings() ) retriever = vectorstore.as_retriever() -
Create a retriever tool using LangChain's prebuilt
create_retriever_tool:from langchain.tools.retriever import create_retriever_tool retriever_tool = create_retriever_tool( retriever, "retrieve_blog_posts", "Search and return information about Lilian Weng blog posts.", ) -
Test the tool:
retriever_tool.invoke({"query": "types of reward hacking"})
3. Generate query
Now we will start building components (nodes and edges) for our agentic RAG graph. Note that the components will operate on the MessagesState — graph state that contains a messages key with a list of chat messages.
-
Build a
generate_query_or_respondnode. It will call an LLM to generate a response based on the current graph state (list of messages). Given the input messages, it will decide to retrieve using the retriever tool, or respond directly to the user. Note that we're giving the chat model access to theretriever_toolwe created earlier via.bind_tools:from langgraph.graph import MessagesState from langchain.chat_models import init_chat_model response_model = init_chat_model("openai:gpt-4.1", temperature=0) def generate_query_or_respond(state: MessagesState): """Call the model to generate a response based on the current state. Given the question, it will decide to retrieve using the retriever tool, or simply respond to the user. """ response = ( response_model # highlight-next-line .bind_tools([retriever_tool]).invoke(state["messages"]) ) return {"messages": [response]} -
Try it on a random input:
input = {"messages": [{"role": "user", "content": "hello!"}]} generate_query_or_respond(input)["messages"][-1].pretty_print()Output:
================================== Ai Message ================================== Hello! How can I help you today? -
Ask a question that requires semantic search:
input = { "messages": [ { "role": "user", "content": "What does Lilian Weng say about types of reward hacking?", } ] } generate_query_or_respond(input)["messages"][-1].pretty_print()Output:
================================== Ai Message ================================== Tool Calls: retrieve_blog_posts (call_tYQxgfIlnQUDMdtAhdbXNwIM) Call ID: call_tYQxgfIlnQUDMdtAhdbXNwIM Args: query: types of reward hacking
4. Grade documents
-
Add a conditional edge —
grade_documents— to determine whether the retrieved documents are relevant to the question. We will use a model with a structured output schemaGradeDocumentsfor document grading. Thegrade_documentsfunction will return the name of the node to go to based on the grading decision (generate_answerorrewrite_question):from pydantic import BaseModel, Field from typing import Literal GRADE_PROMPT = ( "You are a grader assessing relevance of a retrieved document to a user question. \n " "Here is the retrieved document: \n\n {context} \n\n" "Here is the user question: {question} \n" "If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n" "Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question." ) # highlight-next-line class GradeDocuments(BaseModel): """Grade documents using a binary score for relevance check.""" binary_score: str = Field( description="Relevance score: 'yes' if relevant, or 'no' if not relevant" ) grader_model = init_chat_model("openai:gpt-4.1", temperature=0) def grade_documents( state: MessagesState, ) -> Literal["generate_answer", "rewrite_question"]: """Determine whether the retrieved documents are relevant to the question.""" question = state["messages"][0].content context = state["messages"][-1].content prompt = GRADE_PROMPT.format(question=question, context=context) response = ( grader_model # highlight-next-line .with_structured_output(GradeDocuments).invoke( [{"role": "user", "content": prompt}] ) ) score = response.binary_score if score == "yes": return "generate_answer" else: return "rewrite_question" -
Run this with irrelevant documents in the tool response:
from langchain_core.messages import convert_to_messages input = { "messages": convert_to_messages( [ { "role": "user", "content": "What does Lilian Weng say about types of reward hacking?", }, { "role": "assistant", "content": "", "tool_calls": [ { "id": "1", "name": "retrieve_blog_posts", "args": {"query": "types of reward hacking"}, } ], }, {"role": "tool", "content": "meow", "tool_call_id": "1"}, ] ) } grade_documents(input) -
Confirm that the relevant documents are classified as such:
input = { "messages": convert_to_messages( [ { "role": "user", "content": "What does Lilian Weng say about types of reward hacking?", }, { "role": "assistant", "content": "", "tool_calls": [ { "id": "1", "name": "retrieve_blog_posts", "args": {"query": "types of reward hacking"}, } ], }, { "role": "tool", "content": "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering", "tool_call_id": "1", }, ] ) } grade_documents(input)
5. Rewrite question
-
Build the
rewrite_questionnode. The retriever tool can return potentially irrelevant documents, which indicates a need to improve the original user question. To do so, we will call therewrite_questionnode:REWRITE_PROMPT = ( "Look at the input and try to reason about the underlying semantic intent / meaning.\n" "Here is the initial question:" "\n ------- \n" "{question}" "\n ------- \n" "Formulate an improved question:" ) def rewrite_question(state: MessagesState): """Rewrite the original user question.""" messages = state["messages"] question = messages[0].content prompt = REWRITE_PROMPT.format(question=question) response = response_model.invoke([{"role": "user", "content": prompt}]) return {"messages": [{"role": "user", "content": response.content}]} -
Try it out:
input = { "messages": convert_to_messages( [ { "role": "user", "content": "What does Lilian Weng say about types of reward hacking?", }, { "role": "assistant", "content": "", "tool_calls": [ { "id": "1", "name": "retrieve_blog_posts", "args": {"query": "types of reward hacking"}, } ], }, {"role": "tool", "content": "meow", "tool_call_id": "1"}, ] ) } response = rewrite_question(input) print(response["messages"][-1]["content"])Output:
What are the different types of reward hacking described by Lilian Weng, and how does she explain them?
6. Generate an answer
-
Build
generate_answernode: if we pass the grader checks, we can generate the final answer based on the original question and the retrieved context:GENERATE_PROMPT = ( "You are an assistant for question-answering tasks. " "Use the following pieces of retrieved context to answer the question. " "If you don't know the answer, just say that you don't know. " "Use three sentences maximum and keep the answer concise.\n" "Question: {question} \n" "Context: {context}" ) def generate_answer(state: MessagesState): """Generate an answer.""" question = state["messages"][0].content context = state["messages"][-1].content prompt = GENERATE_PROMPT.format(question=question, context=context) response = response_model.invoke([{"role": "user", "content": prompt}]) return {"messages": [response]} -
Try it:
input = { "messages": convert_to_messages( [ { "role": "user", "content": "What does Lilian Weng say about types of reward hacking?", }, { "role": "assistant", "content": "", "tool_calls": [ { "id": "1", "name": "retrieve_blog_posts", "args": {"query": "types of reward hacking"}, } ], }, { "role": "tool", "content": "reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering", "tool_call_id": "1", }, ] ) } response = generate_answer(input) response["messages"][-1].pretty_print()Output:
================================== Ai Message ================================== Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors.
7. Assemble the graph
- Start with a
generate_query_or_respondand determine if we need to callretriever_tool - Route to next step using
tools_condition:- If
generate_query_or_respondreturnedtool_calls, callretriever_toolto retrieve context - Otherwise, respond directly to the user
- If
- Grade retrieved document content for relevance to the question (
grade_documents) and route to next step:- If not relevant, rewrite the question using
rewrite_questionand then callgenerate_query_or_respondagain - If relevant, proceed to
generate_answerand generate final response using theToolMessagewith the retrieved document context
- If not relevant, rewrite the question using
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langgraph.prebuilt import tools_condition
workflow = StateGraph(MessagesState)
# Define the nodes we will cycle between
workflow.add_node(generate_query_or_respond)
workflow.add_node("retrieve", ToolNode([retriever_tool]))
workflow.add_node(rewrite_question)
workflow.add_node(generate_answer)
workflow.add_edge(START, "generate_query_or_respond")
# Decide whether to retrieve
workflow.add_conditional_edges(
"generate_query_or_respond",
# Assess LLM decision (call `retriever_tool` tool or respond to the user)
tools_condition,
{
# Translate the condition outputs to nodes in our graph
"tools": "retrieve",
END: END,
},
)
# Edges taken after the `action` node is called.
workflow.add_conditional_edges(
"retrieve",
# Assess agent decision
grade_documents,
)
workflow.add_edge("generate_answer", END)
workflow.add_edge("rewrite_question", "generate_query_or_respond")
# Compile
graph = workflow.compile()
Visualize the graph:
from IPython.display import Image, display
display(Image(graph.get_graph().draw_mermaid_png()))
8. Run the agentic RAG
for chunk in graph.stream(
{
"messages": [
{
"role": "user",
"content": "What does Lilian Weng say about types of reward hacking?",
}
]
}
):
for node, update in chunk.items():
print("Update from node", node)
update["messages"][-1].pretty_print()
print("\n\n")
Output:
Update from node generate_query_or_respond
================================== Ai Message ==================================
Tool Calls:
retrieve_blog_posts (call_NYu2vq4km9nNNEFqJwefWKu1)
Call ID: call_NYu2vq4km9nNNEFqJwefWKu1
Args:
query: types of reward hacking
Update from node retrieve
================================= Tool Message ==================================
Name: retrieve_blog_posts
(Note: Some work defines reward tampering as a distinct category of misalignment behavior from reward hacking. But I consider reward hacking as a broader concept here.)
At a high level, reward hacking can be categorized into two types: environment or goal misspecification, and reward tampering.
Why does Reward Hacking Exist?#
Pan et al. (2022) investigated reward hacking as a function of agent capabilities, including (1) model size, (2) action space resolution, (3) observation space noise, and (4) training time. They also proposed a taxonomy of three types of misspecified proxy rewards:
Let's Define Reward Hacking#
Reward shaping in RL is challenging. Reward hacking occurs when an RL agent exploits flaws or ambiguities in the reward function to obtain high rewards without genuinely learning the intended behaviors or completing the task as designed. In recent years, several related concepts have been proposed, all referring to some form of reward hacking:
Update from node generate_answer
================================== Ai Message ==================================
Lilian Weng categorizes reward hacking into two types: environment or goal misspecification, and reward tampering. She considers reward hacking as a broad concept that includes both of these categories. Reward hacking occurs when an agent exploits flaws or ambiguities in the reward function to achieve high rewards without performing the intended behaviors.

