Search⌘ K
AI Features

Building a Working Agent

Explore how to build a working research assistant agent by integrating embeddings, vector stores, and a generative AI model. Learn to set up tool registration with validated schemas and enforce guardrails to ensure safe tool usage. Understand how to manage bounded loops for retrieval and answer generation, maintain traceability of inputs, outputs, and intermediate steps, and evaluate agent responses. This lesson helps you implement a reliable, debuggable AI agent with controlled tool calls and step limits.

A working Research Assistant agent is easiest to debug when one user question triggers exactly one retrieval tool call and exactly one final answer that includes citations. That single bounded pass makes every boundary visible, from the raw user input to the retrieved document ids to the formatted output schema.

This lesson covers one end-to-end run, then adds a step loop, a small checkpoint, and tool guardrails while keeping strict step limits and traceability.

To make the baseline concrete, walk through the pieces that make up one end-to-end pass: embeddings and vector store setup, tool registration with a validated schema, the Gemini model, and the prebuilt agent that wires them together.

from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
embeddings = HuggingFaceEmbeddings(model_name="sentence-transformers/all-MiniLM-L6-v2")
vectorstore = Chroma.from_texts(
texts=[
"Quantum computing relies on qubits which can exist in superposition states, allowing parallel processing.",
"Transformer architectures rely on self-attention mechanisms to dynamically weigh token importance.",
"LangGraph enables stateful multi-actor applications built with LLMs by modeling workflows as graphs.",
"Retrieval-Augmented Generation (RAG) reduces hallucination by grounding responses in retrieved contexts."
],
metadatas=[
{"chunk_id": "doc_101", "source": "Intro to Quantum Computing"},
{"chunk_id": "doc_102", "source": "Attention Is All You Need Paper"},
{"chunk_id": "doc_103", "source": "LangGraph Orchestration Guide"},
{"chunk_id": "doc_104", "source": "RAG Architecture Review"}
],
embedding=embeddings,
collection_name="internal_docs",
persist_directory="./chroma_store"
)
Vector store and embeddings
  • Line 4 (embeddings = HuggingFaceEmbeddings(...)): Loads a real pretrained embedding model.

  • Lines 6–20 (vectorstore = Chroma.from_texts(...)): Builds and persists the vector store directly from four sample text chunks and their metadata in one call, rather than opening an existing empty collection separately.

from langchain_core.tools import tool
from pydantic import BaseModel, Field, ConfigDict
class RetrieveArgs(BaseModel):
model_config = ConfigDict(extra="forbid")
query: str = Field(max_length=500, description="Search query.")
k: int = Field(default=5, ge=1, le=20, description="Number of documents to retrieve.")
@tool("retrieve", args_schema=RetrieveArgs)
def retrieve(query: str, k: int = 5) -> dict:
"""Retrieve relevant documents for a query from the Research Assistant's index."""
docs = vectorstore.similarity_search(query, k=k)
return {
"documents": [
{
"id": d.metadata["chunk_id"],
"text": d.page_content,
"title": d.metadata.get("source", "Unknown")
}
for d in docs
]
}
Retrieve arguments
  • Line 1 (from langchain_core.tools import tool): The decorator that turns a plain function into a schema-carrying tool object.

  • Lines 4–7 (class RetrieveArgs): Declares the argument contract once — bounded query length, bounded k, and extra="forbid" rejecting stray keys.

  • Line 9 (@tool("retrieve", args_schema=RetrieveArgs)): Attaches the name "retrieve" and schema to the function.

  • Lines 10–21 (def retrieve(...)): The real handler — runs a real similarity search against the live vector store and returns normalized documents, defaulting title to "Unknown" when metadata omits it.

from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.agents import create_agent
llm = ChatGoogleGenerativeAI(
model="gemini-2.5-flash",
api_key="{{GEMINI_API_KEY}}",
temperature=0
)
SYSTEM_PROMPT = """
You are a research assistant that answers questions using an internal knowledge base.
Use the retrieve tool whenever the question requires information from the knowledge base.
Base your answer on the information returned by the tool.
Do not invent facts or document IDs.
If the retrieved information is insufficient to answer the question, explain that the available knowledge base does not contain enough information.
"""
agent = create_agent(
model=llm,
tools=[retrieve],
system_prompt=SYSTEM_PROMPT
)
Agent
  • Lines 4–8 (llm = ChatGoogleGenerativeAI(...)): The real chat model client — gemini-2.5-flash, temperature 0 for deterministic output.

  • Lines 10–18 (SYSTEM_PROMPT): Fixes the model's behavior and guardrails in plain text: use retrieve when the knowledge base is needed, ground answers strictly in retrieved content, never invent facts or doc ids, and say explicitly when the retrieved context is insufficient. This prompt is doing the grounding and citation-discipline work that the earlier manual pipeline enforced through a GroundedAnswer schema and a code-level citation formatter — here, that discipline is asked for in the prompt ...