When it does not know
An assistant that answers everything is worse than one that answers most things and says so about the rest. The fix is a conditional edge from lesson 6.
Everything from the last lesson stays. Assume DOCS, search, the state and the two nodes are already in the file.
Ask one question about what was found
def did_we_find_anything(state):
return "answer" if state["found"] else "say_no"It is the same shape as pick_team in lesson 6. Look at the state, return the name of the node that should run next.
Somewhere to go when the answer is no
def say_no(state):
return {"answer": "I could not find anything about that in the documentation."}No model call. There is nothing to answer from, so there is nothing to ask a model about, and a fixed sentence is both cheaper and more honest than anything it would produce.
Say where the answer came from
While you are here, put the file name on the answer. A documentation assistant that cannot tell you which page it read is asking to be trusted for no reason.
from pretend_model import PretendModel
from langchain_core.messages import HumanMessage
def answer(state):
prompt = f"Context: {state['found']}\n\nQuestion: {state['question']}"
reply = PretendModel().invoke([HumanMessage(prompt)]).content
return {"answer": f"{reply}\n\nSource: {state['source']}"}Rewire it with the branch
from langgraph.graph import StateGraph, START
builder = StateGraph(State)
builder.add_node("retrieve", retrieve)
builder.add_node("answer", answer)
builder.add_node("say_no", say_no)
builder.add_edge(START, "retrieve")
builder.add_conditional_edges("retrieve", did_we_find_anything, ["answer", "say_no"])
graph = builder.compile()One edge became a branch, and the graph now has two ways to finish. Nothing else about it changed.
for question in ["How do I reset my password?", "Who won the cricket?"]:
result = graph.invoke({"question": question, "found": "", "source": "", "answer": ""})
print(f"Q: {question}\n{result['answer']}\n")One answer with a source under it, and one honest refusal. The refusal cost nothing, because no model was called on the path that had nothing to say.
What you have actually built
That is a working documentation assistant, and it is about forty lines all together. Everything in it is something you learned separately: a state, two nodes, a conditional edge, a model call and a prompt built from retrieved text.
It also has the property people care about most and rarely get. When it does not know, it says so, and when it does know, it tells you where it read it.
The other way to build this
Here the search always runs, before the model gets a say. The alternative is to hand the model a search tool, as in lesson 18, and let it decide whether to look anything up.
| Search as a node | Search as a tool | |
|---|---|---|
| Who decides | You do. It always runs. | The model does, each time. |
| Cost | One model call | At least two, sometimes more |
| Good for | Questions about a known set of documents | Assistants that also chat, calculate, or act |
| Predictable | Completely | As predictable as the model |
Neither is more advanced than the other. For a documentation assistant the node is usually the right answer, because you already know every question is about the documents.
- Ask something half covered, such as how long a reset link lasts, and see which page it uses.
- Make
say_nosuggest the closest page anyway, and decide whether you prefer it. - Turn the search into a tool with
@tooland give it to the agent loop from lesson 18.
This is what real progress feels like.