Agent UX
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
This page’s examples target Python 3.12 with LangChain 1.4.2 / langchain-core 1.6.5 / LangGraph 1.2.12 and
langchain-ollama 1.1.0, Java 21 with LangChain4j 1.20.2, and Spring Boot 4.1 with Spring AI 2.0.1, using
DocsAssistant's Ollama llama3.1:8b chat model, as checked on 2026-09-28 (see the
AI Agents baseline).
An agent’s quality is only half of its usefulness; the other half is whether people can tell what it is doing, how much to trust it, and what to do when it is wrong. Agents make this harder than chat: they take longer, they act, and they are sometimes confidently wrong. This page paraphrases the agent-UX guidance in one of the books listed in the bibliography and applies it to DocsAssistant. The protocol side of connecting an agent to a front end — the open AG-UI (Agent-User Interaction) protocol for streaming agent events to user interfaces, and chat channels in general — belongs to the Conversational Channels sub-section of this AI section (see Chat UI Protocols).
Synchronous and asynchronous interaction
-
Synchronous — the user asks and waits, as in chat. Works when the agent answers in seconds; beyond that, the user needs to see progress or they assume it is broken.
-
Asynchronous — the user delegates and leaves; the agent works in the background and reports back (a notification, a pull request, an email). Right for tasks that take minutes or hours, like deep research or "check every page for broken Spring AI links". It needs a place to see pending and finished tasks, a way to cancel, and results that make sense out of context, since the user has moved on.
Many agents need both: DocsAssistant answers questions synchronously but files issue reports asynchronously, since they wait for a maintainer’s approval (see Human-in-the-Loop). Pick per task, and tell the user which mode they are in ("I’ll let you know when a maintainer has reviewed this").
Streaming intermediate steps
For synchronous agents, streaming is the difference between "it’s thinking" and "it’s broken". Stream three kinds of events, each with a different UI treatment:
-
Step updates — which step just finished (searched, read a page, drafted), shown as a compact activity log the user can expand;
-
Progress messages — short, human-readable status from inside long tools ("Searching the docs for 'chunking'…"), shown as a status line;
-
Answer tokens — the final answer as it is generated, so reading can start immediately.
Keep raw tool output and chain-of-thought out of the default view — summarise steps in plain language, and let curious users expand the detail.
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_ollama import ChatOllama
from langgraph.config import get_stream_writer
from docsassistant.search import search_docs
@tool
def docs_search_verbose(query: str) -> list[dict]:
"""Search this site's documentation for chunks relevant to `query`."""
writer = get_stream_writer()
writer({"status": f"Searching the docs for '{query}'..."}) # a custom progress event
hits = search_docs(query, k=5)
writer({"status": f"Found {len(hits)} sections; reading the best ones."})
return hits
agent = create_agent(ChatOllama(model="llama3.1:8b", temperature=0), tools=[docs_search_verbose])
for mode, chunk in agent.stream(
{"messages": [("user", "How does DocsAssistant chunk AsciiDoc pages?")]},
stream_mode=["updates", "messages", "custom"],
):
if mode == "custom":
print("[progress]", chunk["status"]) # show as a status line
elif mode == "updates":
for step, update in chunk.items(): # one entry per finished step
print("[step]", step)
elif mode == "messages":
token, metadata = chunk # LLM tokens as they arrive
if metadata["langgraph_node"] == "model" and token.content:
print(token.content, end="", flush=True)
stream_mode accepts a list, and each chunk then arrives as a (mode, data) pair: updates yields each step’s
output as it finishes, messages yields model tokens with metadata (the emitting node’s name, model for
create_agent’s model node), and `custom yields whatever tools or nodes send through get_stream_writer(). See
LangChain — Agents (streaming) and
LangGraph v1 release notes.
TokenStream with tool-execution eventsinterface StreamingDocsAssistant {
TokenStream chat(String question);
}
StreamingDocsAssistant assistant = AiServices.builder(StreamingDocsAssistant.class)
.streamingChatModel(streamingChatModel) // OllamaStreamingChatModel
.tools(new DocsTools())
.build();
assistant.chat("How does DocsAssistant chunk AsciiDoc pages?")
.onToolExecuted(t -> ui.status("Used " + t.request().name())) // step updates
.onPartialResponse(ui::appendToken) // answer tokens
.onCompleteResponse(r -> ui.done())
.onError(ui::showError)
.start();
See LangChain4j — AI Services (streaming).
@RestController
class DocsStreamController {
private final ChatClient docsAssistant;
DocsStreamController(ChatClient docsAssistant) {
this.docsAssistant = docsAssistant;
}
@GetMapping(path = "/ask", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> ask(@RequestParam String q) {
return docsAssistant.prompt().user(q).stream().content();
}
}
Tool calls run inside the stream, so the tokens pause while a tool executes; a progress event for that gap is a
ToolCallback wrapper or tool method that publishes to the same SSE channel. See
Spring AI — ChatClient (streaming responses).
Communicating confidence and uncertainty
An agent that sounds equally sure about everything teaches users either to trust it blindly or not at all. Make uncertainty visible and specific:
-
show the sources behind every claim, as links the user can check — the single most effective trust signal;
-
show a coarse confidence (high / medium / low) rather than a false-precision percentage, and say what is uncertain ("the docs don’t say which version introduced this");
-
distinguish "the docs say X" from "I infer X".
Structured output makes this a contract rather than a hope: the agent must return the fields the UI renders.
from typing import Literal
from langchain.agents import create_agent
from langchain_ollama import ChatOllama
from pydantic import BaseModel, Field
class DocsAnswer(BaseModel):
answer: str
sources: list[str] = Field(description="page paths the answer is based on")
confidence: Literal["high", "medium", "low"]
unsure_about: str | None = Field(None, description="what the sources did not settle, if anything")
needs_clarification: str | None = Field(None, description="a question to ask the user instead of guessing")
agent = create_agent(
ChatOllama(model="llama3.1:8b", temperature=0),
tools=[docs_search],
response_format=DocsAnswer,
system_prompt="Answer only from the documentation. If the question is ambiguous, set "
"needs_clarification instead of guessing. Rate confidence honestly.",
)
reply = agent.invoke({"messages": [("user", "Which index should I use?")]})["structured_response"]
if reply.needs_clarification:
ui.ask(reply.needs_clarification) # ask for guidance rather than guess
else:
ui.show(reply.answer, sources=reply.sources, badge=reply.confidence, caveat=reply.unsure_about)
response_format makes the agent finish with a validated DocsAnswer in structured_response; ui stands for
the application’s front end. See
LangChain — Structured output. In Spring AI the
same contract is .call().entity(DocsAnswer.class), and in LangChain4j an AI-service method returning a
DocsAnswer record.
Self-reported confidence is a signal, not a measurement: models are frequently over-confident. Calibrate it against labelled cases before showing it, and prefer signals that are harder to fake — whether retrieval found anything above a score threshold, whether several samples agree (see escalation thresholds).
Asking for guidance
When a request is ambiguous ("which index should I use?"), a clarifying question beats a confident guess — but
only if it is one question, specific, and ideally with options ("Which database: pgvector, Qdrant or FAISS?").
Ask early, before expensive work; batch what you need to know into a single question; and carry the answer into
memory so the user is not asked again (see Memory). The needs_clarification field
above is the mechanism; the skill is in the prompt, which should say when to ask and when to just proceed with a
stated assumption.
Failing gracefully
Agents fail: a tool is down, nothing relevant is found, a budget runs out, a policy denies an action. The user experience of failure decides whether they come back:
-
say what happened and what was tried, in plain language ("I searched the vector-rag and Spring AI pages but found nothing about Milvus");
-
offer the next step — a narrower question, a link to browse, a human to contact;
-
keep partial results — if three of four sub-questions were answered, show those three;
-
never fake success — an agent that says "I’ve filed the issue" when the tool returned an error destroys trust faster than any other failure (the actionable error results on Tool Design make this avoidable);
-
make undo and correction easy for anything the agent changed.
Trust calibration
The goal is not maximum trust but calibrated trust: users should rely on the agent where it is reliable and check it where it is not. Trust is built by predictability (the agent behaves the same way for the same request), transparency (sources, steps, reasons), and a track record the user can see; it is lost fastest by confident errors and by actions the user did not expect. Two practical consequences: start new users and new capabilities at low autonomy and raise it as the track record grows (the autonomy slider on Human-in-the-Loop), and after any visible mistake, show what changed so it will not recur.
Proactive vs. intrusive behaviour
A proactive agent acts or suggests without being asked: DocsAssistant noticing that a page it just cited contradicts the Spring AI reference, and offering to report it. Done well, this is where agents feel most useful; done badly, it is noise. Guidelines:
-
be proactive only when the value is high and the confidence is high;
-
suggest rather than act, and make ignoring the suggestion free;
-
respect focus — surface suggestions at natural pauses, not mid-task;
-
let users tune or turn it off, and learn from dismissals;
-
never be proactive with write actions: a suggestion to file an issue is fine, filing it unasked is not.
In other frameworks
| Framework | UX-relevant support |
|---|---|
OpenAI Agents SDK |
|
Claude Agent SDK |
|
Microsoft Agent Framework |
Streaming agent response updates, workflow events, and an AG-UI integration for front ends. |
CrewAI |
Streaming output and an event bus for step and task events; Flows can pause for human feedback. |