Створення циклу покращення агента за допомогою LangGraph та оцінок LangSmith.
Крок за кроком цикл LangGraph та LangSmith: збір слідів, три рівні оцінювання, кластерування невдач, набори даних для регресії офлайн та попередні оцінки перед випуском.
Джерела: Architecting the Agent Improvement Loop: Production-Grade Eval Pipelines
Щоб агент почав працювати, потрібно найменше зусиль. Але саме покращення його функціоналу згодом відрізняє хобі-рішення від стабільної системи. Багато команд випускають першу версію, а потім місяцями відповідають на кожну скаргу окремо. Кожен патч — це реакція на окремий випадок, кожне виправлення залишається локальним, і агент у цілому ніколи не покращується. Так накопичуються тимчасові рішення.
Зазвичай причиною є сприйняття агента як звичайного програмного забезпечення. У звичайних програмах код пояснює, що робить система, тож для пошуку багу достатньо прочитати вихідний код. Але з агентами таке припущення не спрацьовує. Код описує лише те, що системі дозволено робити, тоді як записи фіксують те, що вона насправді робила. Саме ця різниця є основою кожної серйозної системи оцінки, включаючи ті, що Anthropic описує у своїх технічних матеріалах, та ті, що LangChain описує у документації LangSmith.
Нижче наведено повний цикл вдосконалення для агента LangGraph. Він починається з невеликого агента ReAct, записує його сліди в LangSmith, додає три рівні оцінювання, виявляє повторювані моделі невдач, перетворює ці невдачі на постійний офлайн-набір тестів та проводить оцінку регресії перед тим, як буде випущено будь-яке виправлення. Кожен етап супроводжується кодом, поясненням та очікуваним результатом.
Цикл працює наступним чином:
- Агент обробляє реальний запит користувача та створює запис, який охоплює кожен виклик LLM, виклик інструменту та проміжний результат.
- Перевірки на основі коду оцінюють кожен запис з точки зору формату та коректності використання інструменту, суддя з LLM оцінює корисність та релевантність, а люди переглядають менш очевидні недоліки.
- Записи з низькими оцінками групуються за моделями невдач, такими як використання неправильного інструменту, зміна логіки міркувань чи неправильне розуміння наміру користувача.
Саме так агент з часом стає кращим, а не просто накопичує патчі. Існує шість етапів:
- Етап 1, основа: агент LangGraph ReAct із трекінгом через LangSmith.
- Етап 2, збір даних: отримання результатів роботи в продакшн у вигляді структурованих записів.
- Етап 3, оцінка: перевірка коду, оцінка за допомогою LLM та людський огляд.
- Етап 4, виявлення закономірностей: перетворення оцінених даних на категорії несправностей.
- Етап 5, набори тестів офлайн: перетворення несправностей у набір даних, стійкий до регресії.
Етап 1: агент-основа
На першому етапі створюється невеликий агент підтримки клієнтів, який відповідає на запитання щодо облікового запису. Він має два інструменти: один для пошуку деталей облікового запису та інший для перевірки останніх замовлень, і використовує готовий агент ReAct від LangGraph, що є стандартним шаблоном для агентів з викликом інструментів у цьому середовищі. Функція трасування LangSmith увімкнена, тож кожен запуск, виклик LLM та виклик інструменту автоматично записуються.
Для цього потрібні три елементи:
- LangGraph StateGraph є основною структурою, яка забезпечує зв’язок між вузлом, що вирішує, що робити, та вузлом, який виконує інструмент. Допоміжна функція
create_react_agentобгортає цей шаблон.
LANGSMITH_TRACING=true. Після цього кожен виклик LangChain або LangGraph генерує запис про відстеження без змін у коді.@tool з бібліотеки LangChain, які роблять кожен з них доступним для агента разом із ім’ям та описом, що допомагають у виборі інструменту.Налаштування середовища
Спочатку необхідно встановити бібліотеки:
# Install the libraries we need
pip install langgraph langchain langchain-openai langsmith
Встановлюється чотири пакети. langgraph забезпечує середовище виконання графів та готового агента ReAct. langchain надає декоратор @tool та типи повідомлень. langchain-openai обгортає модель чату OpenAI в інтерфейс LangChain. langsmith — це бібліотека клієнта, яка використовується для зв’язку з бекендом LangSmith з метою відстеження та оцінки.
Далі йдуть змінні середовища, які увімкнюють функцію відстеження без змін у коді:
export LANGSMITH_TRACING=true
export LANGSMITH_API_KEY=your_langsmith_key
export LANGSMITH_PROJECT=agent-improvement-loop
export OPENAI_API_KEY=your_openai_key
LANGSMITH_TRACING=true — це перемикач. LANGSMITH_PROJECT визначає назву проекту, який групує сліди, завдяки чому їх можна пізніше фільтрувати. Ще два параметри — це облікові дані для LangSmith та постачальника моделей.
Після налаштування цих чотирьох змінних кожен виклик LangChain або LangGraph у процесі надсилає слід до LangSmith. Не потрібно нічого обгортати чи додавати додаткові елементи, оскільки відстеження відбувається у фоновому режимі.
Створення агента
Спочатку імпорти:
# Step 1: Import the pieces we need
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
Імпорти включають три елементи. ChatOpenAI обгортає API для завершення чату в OpenAI для LangChain. Декоратор tool перетворює звичайну функцію Python на функцію, яку може використовувати агент. create_react_agent — це готовий помічник LangGraph, який створює граф у стилі ReAct для виклику інструментів.
Далі йдуть два інструменти:
# Step 2: Define two tools the agent can call
@tool
def get_account_info(account_id: str) -> str:
"""Look up basic information for an account by account_id."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
@tool
def get_recent_orders(account_id: str) -> str:
"""Return the three most recent orders for a given account_id."""
fake_orders = {
"A100": "Orders for A100: #9001 shipped, #9002 processing, #9003 returned",
"A200": "Orders for A200: no orders in the last 90 days",
}
return fake_orders.get(account_id, f"No orders for {account_id}")
Кожен інструмент приймає account_id та повертає рядок. Документація до кожної функції має велике значення, адже ШІ читає цей текст під час вирішення, який інструмент викликати. Декоратор отримує назву функції, параметри та документацію, щоб створити схему, яку бачить модель. У реальній системі ці функції б здійснювали запити до бази даних чи API, але в демонстраційній версії повертаються жорстко закодовані рядки, щоб система залишалася самодостатньою та відтворюваною.
Нарешті агент формується:
# Step 3: Wire the tools into a ReAct agent
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
tools = [get_account_info, get_recent_orders]
agent = create_react_agent(model, tools)
Модель чату створюється з температурою нуль для досягнення відтворюваності. Інструменти потрапляють у список та передаються разом із моделлю до функції create_react_agent, яка повертає скомпільований LangGraph StateGraph, у якому вже є крок міркування, крок виконання інструменту та умовні зв’язки, що їх поєднують.
Тестування агента
Просте запитання дозволяє його протестувати:
# Step 4: Invoke the agent with a simple query
result = agent.invoke({
"messages": [{"role": "user", "content": "What plan is account A100 on?"}]
})
for message in result["messages"]:
print(message.type, ":", message.content)
У виклику передається словник, який містить ключ messages. Це тому, що функція create_react_agent спирається на заздалегідь створену схему MessagesState, стан якої — це просто список повідомлень у чаті, який агент розширює під час обробки інформації та використання інструментів. Після цього цикл виводить кожен тип повідомлення разом із його змістом, тож весь хід виконання є видимим.
Очікуваний результат:
human : What plan is account A100 on?
ai :
tool : Account A100: plan=pro, status=active, email=ada@example.com
ai : Account A100 is on the pro plan.
Агент отримав повідомлення від користувача, потім надіслав повідомлення від ШІ, яке ініціювало виклик інструменту (його зміст порожній, оскільки воно містить лише виклик інструменту без тексту). Інструмент повернув рядок з даними про обліковий запис, і агент завершив роботу відповіддю. При запуску LangSmith зараз видно один новий слід у проєкті agent-improvement-loop, де записано кожен крок.
У цьому коді нічого не було для фіксації сліду. Як тільки у середовищі встановлено LANGSMITH_TRACING=true, кожен виклик надсилає вхідні дані, результати та проміжні кроки до LangSmith. Саме ця можливість спостереження є основою для подальшої роботи цього циклу.
На першому етапі агент, який використовує інструменти, працює під керуванням LangGraph, і при кожному запуску створюється слід. Однак слід, який знаходиться у панелі керування, ще не є сигналом про покращення. Його потрібно витягнути та проаналізувати, що є завданням наступного етапу.
Етап 2: збір слідів
На цьому етапі сліди отримуються з LangSmith та передаються у код для аналізу. Слід – це один повний запуск агента з вхідними даними, вихідними даними, викликами інструментів та показником затримки. Клієнт LangSmith на Python перелічує запуски з проекту, фільтрує їх за часовим проміжком та статусом та перетворює їх на об’єкти, які може використовувати етап оцінювання.
Кілька термінів допомагають краще зрозуміти код:
- Запуск – це окрема частина записаної роботи, або це повний виклик агента, або один виклик LLM, розміщений всередині нього. Одне викликання агента утворює дерево запусків.
- Кореневий запуск – це запуск найвищого рівня одного виклику агента; саме він перелічується, коли потрібен один запис на запит користувача.
- Проект – це простір імен, до якого належить запуск; він визначається через
LANGSMITH_PROJECT. Фільтрація за проектом дозволяє розділити запуски у продакшені від запусків під час локальної розробки.
Отримання останніх запусків
Спочатку створюється клієнт:
# Step 1: Create a LangSmith client
from langsmith import Client
from datetime import datetime, timedelta
client = Client()
Client імпортується з пакета langsmith та створюється без аргументів. Він самостійно зчитує значення LANGSMITH_API_KEY з середовища, тому жодні облікові дані не передаються.
Потім відображається список останніх запусків з правами root:
# Step 2: List recent root runs from our project
recent_runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
print(f"Found {len(recent_runs)} root runs in the last 24 hours")
client.list_runs викликається з трьома фільтрами. project_name обмежує пошук проектом, is_root=True зберігає лише виклики агента найвищого рівня, а не кожен вкладений виклик LLM чи інструменту, а start_time обмежує результати останніми 24 годинами. Ітератор є „лінивим“, тому list() змушує його стати справжнім списком.
Очікуваний результат:
Found 42 root runs in the last 24 hours
Зберігання лише важливих полів
Прямий запуск LangSmith містить багато полів, але для оцінки потрібні лише п’ять елементів: вхідний даний, вихідні дані, час виконання, будь-які помилки та результати додаткових запусків, які вказують на використані інструменти.
# Step 3: Build a compact trace record from each run
def compact_trace(run):
return {
"run_id": str(run.id),
"inputs": run.inputs,
"outputs": run.outputs,
"error": run.error,
"latency_ms": (run.end_time - run.start_time).total_seconds() * 1000
if run.end_time else None,
"tool_calls": [
child.name for child in client.list_runs(
trace_id=run.trace_id, run_type="tool"
)
],
}
traces = [compact_trace(run) for run in recent_runs]
print(f"Collected {len(traces)} compact traces")
print("Example tool calls:", traces[0]["tool_calls"])
Цей помічник приймає сирі дані виконання та перетворює їх на невеликий словник. Вхідні та вихідні дані (run.inputs, run.outputs) містять запит користувача та кінцеву відповідь, тоді як run.error дорівнює None у разі успіху та є рядком у разі невдачі. Затримка виникає внаслідок віднімання end_time від start_time. Для виклику інструментів потрібен ще один запит до LangSmith із параметром run_type="tool" та тим самим trace_id, що дає інформацію лише про виконання інструментів у цій ієрархії виконань.
Очікуваний вихід:
Collected 42 compact traces
Example tool calls: ['get_account_info']
Ідея не полягає у копіюванні даних з LangSmith та їх відмові. Компактна версія просто зручна для оцінки, тоді як повні дані залишаються в LangSmith для детальної налагодки. Зберігання деталізованого перегляду в панелі керування та спрощеного — для аналізу — є розумним способом розділення функцій спостережуваності та оцінки.
Перевірка збирача даних
# Step 4: Inspect the first trace end to end
import json
print(json.dumps(traces[0], indent=2, default=str))
Перший слід зберігається у вигляді відступлених даних JSON за допомогою json.dumps. Аргумент default=str змушує функцію json перетворювати на рядок все, що вона не може серіалізувати сама, що стосується типів datetime та UUID, які повертає LangSmith.
Очікуваний результат:
{
"run_id": "b1d2e3f4-...",
"inputs": {"messages": [{"role": "user", "content": "What plan is account A100 on?"}]},
"outputs": {"messages": [{"role": "ai", "content": "Account A100 is on the pro plan."}]},
"error": null,
"latency_ms": 1423.51,
"tool_calls": ["get_account_info"]
}
Кожна партія продукції тепер має чистий, структурований запис, який на наступному етапі буде доповнений оцінками. Збір слідів коштує недорого, а справжня праця полягає у тому, щоб зробити їх корисними. Багато команд зупиняються на панелі керування та дивуються, чому можливості моніторингу ніколи не перетворюються на покращення. Сліди без оцінок є шумом, а слід без оцінки — це просто рядок журналу.
Етап 3: оцінювання слідів
Три рівні оцінювання формують один доповнений слід. Стаття інженерів Anthropic про оцінювання описує саме таку структуру: оцінювачі на основі коду, які є швидкими, об’єктивними та відтворюваними; оцінювачі на основі моделей, які є гнучкими та масштабованими; а також людські оцінювачі, які є золотим стандартом, але дорогими. Тут створюється по одному представнику кожного типу.
- Перевірки на основі коду — це швидкі детерміністичні функції, які перевіряють структурні факти, такі як те, чи використав агент правильний інструмент, чи має результат очікуваний формат. Вони безкоштовно виявляють очевидні помилки.
- Суддя у вигляді LLM — це друга модель, яка оцінює суб’єктивні якості, такі як корисність, релевантність та тон. Вона може працювати з кожним записом, але її потрібно налаштувати відповідно до людського судження, перш ніж їй можна буде довіряти.
- Людський огляд дозволяє експерту з певної галузі переглянути записи, які автоматизація пропустила або оцінила непослідовно. Це є базою правди, за якою налаштовуються інші два рівні.
Рівень 1: правильність інструменту
# Step 1: Define a rule that says which tool SHOULD be called for a given query
import re
def expected_tool_for_query(query: str) -> str:
q = query.lower()
if re.search(r"\b(order|shipment|shipped|return)\b", q):
return "get_recent_orders"
if re.search(r"\b(plan|account|status|email)\b", q):
return "get_account_info"
return "any"
def score_tool_correctness(trace):
query = trace["inputs"]["messages"][0]["content"]
expected = expected_tool_for_query(query)
actual = trace["tool_calls"][0] if trace["tool_calls"] else None
if expected == "any":
return 1.0
return 1.0 if actual == expected else 0.0
Визначено дві функції. expected_tool_for_query застосовує регулярні вирази до тексту запиту, щоб здогадатися, який інструмент мав би ним опікуватися. Це примітивна евристика, яка підходить для виявлення очевидних помилок маршрутизації, але не підходить для продакшну. score_tool_correctness бере запит користувача зі сліду, визначає очікуваний інструмент та порівнює його з першим інструментом, який насправді використав агент. Невідповідність оцінюється як 0.0, а відповідність — як 1.0.
У версії для продакшну регулярні вирази можна замінити на більш потужний класифікатор або двигун правил, орієнтований на наміри користувача. Принцип залишається незмінним: спочатку виконується детермінована перевірка, а вже потім — дорогі оцінювачі на основі LLM.
Рівень 2: суддя з LLM для оцінки корисності
# Step 2: Score qualitative properties with an LLM judge
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
judge_model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
judge_prompt = ChatPromptTemplate.from_messages([
("system", "You are an expert reviewer of AI agent responses. "
"Rate the response on a scale of 1 to 5 for helpfulness. "
"Respond with only a single integer, nothing else."),
("user", "User question: {question}\n\nAgent response: {response}\n\nScore:"),
])
def score_helpfulness(trace):
question = trace["inputs"]["messages"][0]["content"]
response = trace["outputs"]["messages"][-1]["content"]
chain = judge_prompt | judge_model
result = chain.invoke({"question": question, "response": response})
try:
return int(result.content.strip()) / 5.0
except ValueError:
return 0.0
Невеликий ланцюг LangChain створюється шляхом передачі шаблону запиту до моделі-судді. Повідомлення системи обмежує суддю можливістю повертати лише одне ціле число від 1 до 5, тож результат обробляється детерміновано. У функції score_helpfulness запит користувача та кінцева відповідь агента отримуються з запису виконання, запускається ланцюг, а ціле число масштабується до числа з дробом від 0 до 1, щоб його можна було легко поєднати з оцінкою інструменту. gpt-4o-mini використовується як суддя через свою низьку вартість та швидкість роботи.
Судді на основі LLM є стохастичними, тому два запуски над однаковим записом можуть дати трохи різні оцінки. Рекомендації компанії Anthropic передбачають калібрування судді за допомогою набору даних, позначених людьми, перш ніж починати покладатися на неї, і саме такий набір створюється у третьому шарі.
Шар 3: людський огляд як черга анотацій
У LangSmith є вбудована черга анотацій, де користувачі позначають треки у панелі керування. Щоб приклад залишався самодостатнім, та сама структура моделюється у коді.
# Step 3: Model a human review signal
def score_human(trace, human_labels: dict) -> float | None:
run_id = trace["run_id"]
return human_labels.get(run_id)
human_labels = {
"b1d2e3f4-...": 1.0,
"c2e3f4g5-...": 0.0,
}
Функція шукає ідентифікатор виконання у словнику з міток, наданих людьми. Якщо така мітка існує, вона повертається, а якщо ні — функція повертає None, що означає, що виконання ще не було переглянуте. У продакшені цей словник знаходитиметься за API черги анотацій LangSmith. Важливим є те, як поводяться оцінки: вони є розрідженими, оскільки їх отримують лише деякі записи; вони є авторитетними, тому що переважають над іншими у разі розбіжностей; крім того, вони використовуються для налаштування оцінювача LLM.
Об’єднання трьох шарів
# Step 4: Run all three scorers and attach scores to each trace
def enrich(trace):
trace["scores"] = {
"tool_correctness": score_tool_correctness(trace),
"helpfulness": score_helpfulness(trace),
"human": score_human(trace, human_labels),
}
return trace
enriched = [enrich(t) for t in traces]
print(json.dumps(enriched[0]["scores"], indent=2))
Функція enrich додає всі три оцінки до запису та повертає його назад. Вона застосовується до кожного запису, зібраного під час збору даних, що дає список оброблених записів, кожен з яких містить структурований словник scores. Регулярні вирази, засновані на правилах, генерують значення tool_correctness, оцінювач LLM — значення helpfulness, а human є числом з плаваючою комою або None.
Очікуваний результат:
{
"tool_correctness": 1.0,
"helpfulness": 0.8,
"human": null
}
Три оцінки суперечать одна одній навмисно. Показник правильності інструменту свідчить про те, що було використано правильний інструмент, а оцінювач LLM стверджує, що відповідь заслуговує на 4 з 5 балів. Людський рецензент, який додасться пізніше, може сказати, що інструмент був правильним, але формулювання не відповідало тону бренду. Кожний рівень виявляє те, що залишається непоміченим іншими, тож їх поєднання забезпечує більш повний огляд та достовірні дані за різною ціною.
Тепер кожен запис містить три оцінки: одну, засновану на правилах, одну, оцінену LLM, та, якщо це можливо, одну від людини. Оцінки без структури майже так само корисні, як і їх відсутність, тому далі йдуть шаблони.
Етап 4: пошук шаблонів невдач
Як тільки сліди містять показники, та сама проблема схильна до повторення. Агент обирає неправильний інструмент для певної формулювання. Його міркування стають хаотичними під час багатоетапних запитів. Його відповіді технічно правильні, але не відповідають тому, що хотів користувач. Кожен тип проблеми вимагає іншого способу виправлення – чи то перепису опису інструменту, оновлення запиту чи переструктурування робочого процесу. Назвати тип проблеми – це складна частина, тоді як її виправлення зазвичай є простим.
LangChain пропонує агента Insights, створеного саме для вирішення таких проблем у масштабах. Він обробляє сліди, групує їх за причинами невдач та допомагає командам визначити, що потребує уваги першим. Спрощена версія цієї концепції реалізована у коді нижче.
Фільтрація за причинами невдач
# Step 1: Keep only traces where at least one score is low
def is_low_scoring(trace):
scores = trace["scores"]
if scores["human"] is not None and scores["human"] < 0.5:
return True
if scores["tool_correctness"] < 0.5:
return True
if scores["helpfulness"] < 0.6:
return True
return False
failures = [t for t in enriched if is_low_scoring(t)]
print(f"{len(failures)} of {len(enriched)} traces flagged as low scoring")
is_low_scoring виконує три перевірки. Оцінка людини, нижча за 0,5, позначає запис як невдалий. Інакше рішення приймається на основі автоматизованих оцінок: якщо точність інструменту нижча за 0,5, це означає, що агент скерував запит неправильно, а якщо корисність нижча за 0,6, це означає, що суддя-LLM не схвалив відповідь. Пороги можна налаштовувати, і це є розумними значеннями за замовчуванням для першого проходження. Фільтр обробляє доповнені записи з попереднього етапу, щоб сформувати набір невдалих записів.
Очікуваний результат:
9 of 42 traces flagged as low scoring
Групування помилок за категоріями
Тут невелика евристична система присвоює помилкам мітки. У продакшені для цього можна використовувати алгоритми кластеризації, або Insights Agent від LangChain, який саме це робить на основі даних LangSmith.
# Step 2: Tag each failure with a category based on its scores
def categorize(trace):
scores = trace["scores"]
if scores["tool_correctness"] < 0.5:
return "wrong_tool"
if scores["helpfulness"] < 0.6 and scores["tool_correctness"] >= 0.5:
return "unhelpful_answer"
if scores["human"] is not None and scores["human"] < 0.5:
return "human_flagged"
return "other"
from collections import Counter
categories = Counter(categorize(t) for t in failures)
print(categories.most_common())
Функція categorize перевіряє елементи у порядку пріоритету. Неправильний інструмент є найбільш серйозною проблемою, тому його позначають першим. Некорисна відповідь, створена за допомогою правильного інструменту, свідчить про проблеми у формулюванні запиту чи генерації відповіді. Помилки, які людина виявила, а автоматизація пропустила, потрапляють у окрему категорію. Стандартний клас Counter у Python підраховує кількість елементів у кожній категорії, і його імпортують прямо тут, оскільки це його перше використання.
Очікуваний результат:
[('wrong_tool', 5), ('unhelpful_answer', 3), ('human_flagged', 1)]
Категорія вказує на засіб вирішення проблеми. П’ять помилок із використанням неправильного інструменту свідчать про опис інструменту або запит на маршрутизацію. Три некорисні відповіді попри правильний інструмент вказують на запит, який генерує ці відповіді. Помилка, помічена лише людиною, означає, що автоматизовані оцінювачі щось пропустили, тому, ймовірно, потрібен новий оцінювач.
Аналіз окремої закономірності
# Step 3: Print the inputs and outputs for every wrong_tool failure
wrong_tool = [t for t in failures if categorize(t) == "wrong_tool"]
for trace in wrong_tool[:3]:
print("QUERY:", trace["inputs"]["messages"][0]["content"])
print("TOOLS CALLED:", trace["tool_calls"])
print("RESPONSE:", trace["outputs"]["messages"][-1]["content"])
print("---")
Набір помилок звужено до випадків wrong_tool, і цикл виводить запит користувача, інструменти, які фактично були викликані, та кінцеву відповідь для перших трьох. Саме тут людина або більш розумний LLM досліджує ці помилки та формулює гіпотезу, наприклад, що кожен запит, який містить „subscription“, активує get_recent_orders замість get_account_info. Ця гіпотеза стає метою для виправлення.
Очікуваний результат:
QUERY: Is my subscription active?
TOOLS CALLED: ['get_recent_orders']
RESPONSE: I could not find any recent orders to confirm your subscription status.
---
QUERY: Tell me about my subscription to your product
TOOLS CALLED: ['get_recent_orders']
RESPONSE: There are no recent orders for this account.
---
Три приклади на екрані роблять цю закономірність зрозумілою. „Підписка“ семантично ближча до „стану облікового запису“, ніж до „замовлень“, проте агент направляє запити на основі поверхневого збігу ключових слів. Ймовірним рішенням є зміна в одному рядку документації до get_account_info, додавши формулювання про підписку, або додавання короткого системного повідомлення, яке пояснює різницю.
Етап завершується описом шаблонів помилок із підрахунками, категоріями та прикладами. Шаблон, виправлений сьогодні, майже не має цінності, якщо зміни іншого інженера знову його порушать наступного тижня, тому виправлення має бути підтверджене тестом.
Етап 5: навантажувальні тести офлайн
Тут помилки перетворюються на статичні тести. Кожна справжня помилка в продакшені додається до набору даних LangSmith як приклад, що містить вхідні дані (запит користувача), очікувану поведінку (правильний інструмент або прийнятна відповідь) та необов’язкові метадані, такі як категорія помилки. Як тільки дані збережені, вони більше не зникають, тож фіксована помилка не може непомітно повернутися.
Офлайн-оцінка в LangSmith складається з трьох частин:
- Набір даних — це названа колекція прикладів, кожен з яких має вхідні дані та, за бажанням, очікувані результати.
- Цільова функція — це код, який перевіряється; для агента це зазвичай його метод
.invoke. - Оцінювачі — це функції, які оцінюють результати цільової функції для кожного прикладу, схожі на оцінювачі з етапу оцінювання.
Створення набору даних
# Step 1: Create a LangSmith dataset for our agent
dataset_name = "agent-production-failures"
dataset = client.create_dataset(
dataset_name=dataset_name,
description="Production traces where the agent failed. Each example captures the user query and the expected tool call.",
)
print(f"Created dataset: {dataset.id}")
Той самий client з етапу збору даних викликає функцію create_dataset з ім’ям та описом, після чого повертає об’єкт набору даних, чий .id можна буде використовувати пізніше. У реальному використанні це було б захищене перевіркою наявності об’єкта перед створенням, але демонстрація передбачає перший запуск.
Очікуваний результат:
Created dataset: 3f2a1b0c-...
Перетворення невдач на приклади
# Step 2: Convert each failure trace into a dataset example
examples = []
for trace in failures:
query = trace["inputs"]["messages"][0]["content"]
expected_tool = expected_tool_for_query(query)
examples.append({
"inputs": {"question": query},
"outputs": {"expected_tool": expected_tool},
"metadata": {"category": categorize(trace), "source_run_id": trace["run_id"]},
})
client.create_examples(dataset_id=dataset.id, examples=examples)
print(f"Added {len(examples)} examples to {dataset_name}")
Код проходить по кожній сліду помилки та створює прикладний словник із трьома полями. inputs містить запит користувача, outputs — поведінку, яку потрібно перевірити (у цьому випадку — інструмент, який мав бути викликаний), а metadata — категорію та початковий ідентифікатор виконання, щоб кожен приклад можна було простежити до його джерела. Один виклик client.create_examples завантажує все це.
Очікуваний результат:
Added 9 examples to agent-production-failures
У реальних сценаріях впровадження зазвичай дозволяється рецензенту визначати очікуваний результат, оскільки гіпотеза на основі регулярних виразів є лише початковою точкою. Людина, яка схвалює кожен приклад, забезпечує більш надійну основу для порівняння, саме для цього і створена черга анотацій у LangSmith.
Оцінювач регресії
# Step 3: Write an evaluator that checks the agent's tool call against the expected tool
def tool_match_evaluator(inputs: dict, outputs: dict, reference_outputs: dict):
actual_messages = outputs.get("messages", [])
tool_called = None
for m in actual_messages:
if getattr(m, "tool_calls", None):
tool_called = m.tool_calls[0]["name"]
break
expected = reference_outputs["expected_tool"]
score = 1.0 if (expected == "any" or tool_called == expected) else 0.0
return {"key": "tool_match", "score": score}
tool_match_evaluator дотримується схеми, яку очікує LangSmith: він отримує inputs прикладу, фактичні outputs агента та reference_outputs прикладу. Він проглядає повідомлення вихідних даних агента, знаходить перше, що містить виклик інструменту, та видобуває назву цього інструменту. Потім він порівнює її з очікуваною назвою інструменту та повертає словник із key та score. key стає назвою метрики, яка відображається в LangSmith. Структура відповідає офіційному посібнику з швидкого запуску оцінки LangSmith.
Функція-мета
# Step 4: Wrap our agent in a function that takes a LangSmith example and returns its output
def run_agent(inputs: dict) -> dict:
result = agent.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
run_agent навмисно залишається мінімальним. Враховуючи вхідні дані прикладу набору даних, він викликає агента та повертає будь-який сирий словниковий об’єкт, який надходить у відповідь, що відповідає тому, чого очікує функція evaluate в LangSmith. Агент, який викликається, був створений на етапі фундаментування.
Навмисно зберігається проста структура обгортки. Будь-який код, розміщений всередині неї, виконується під час кожної оцінки та збільшує витрати, тоді як просте передавання даних означає, що оцінювач тестує саме агента, а не обгортку.
Отже, набір даних існує, оцінювач підключений, і кожна проблема під час роботи залишається як постійний випадок тестування. На останньому етапі цей оцінювач запускається перед будь-якими змінами.
Етап 6: закриття циклу
Це — брама регресії. Агент отримує зміни, наприклад оновлений опис інструменту, спрямований на усунення помилки маршрутизації „підписки“, виявленої раніше. Перед випуском вся колекція даних про минулі збої проходить обробку за допомогою нової версії. LangSmith зберігає обидві процедури як експерименти та показує, приклад за прикладом, де нова версія покращила ситуацію, а де її результати погіршилися. Випуск відбувається лише тоді, коли новий базовий рівень перевершує старий, і коли раніше усунуті проблеми не повертаються.
Запуск оцінки базового рівня
# Step 1: Run the evaluator across the dataset with the current agent
from langsmith.evaluation import evaluate
experiment_results = evaluate(
run_agent,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v1-baseline",
max_concurrency=4,
)
evaluate імпортується з langsmith.evaluation. Йому передаються мета run_agent, назва набору даних (яку LangSmith отримує з попереднього етапу), список оцінювачів, префікс експерименту для позначення запуску та значення конкурентності чотири, щоб оцінка відбувалася паралельно. Він повертає об’єкт результатів експерименту, який вказує на запуск у LangSmith.
Очікуваний результат:
View the evaluation results for experiment: 'agent-v1-baseline-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.33
Із дев’яти невдалих прикладів поточний агент все ще неправильно направляє більшість з них, і це є базовим рівнем.
Надсилання виправлення та його повторне вимірювання
Виправлення, запропоноване на етапі аналізу шаблонів, полягає у переписуванні документації до get_account_info мовою підписок.
# Step 2: Update the tool docstring to teach the agent about subscriptions
@tool
def get_account_info_v2(account_id: str) -> str:
"""Look up basic information for an account, including plan, status,
subscription tier, and contact email. Use this tool for any question
about the account itself, including subscription status."""
fake_accounts = {
"A100": "Account A100: plan=pro, status=active, email=ada@example.com",
"A200": "Account A200: plan=free, status=suspended, email=turing@example.com",
}
return fake_accounts.get(account_id, f"No account found with id {account_id}")
tools_v2 = [get_account_info_v2, get_recent_orders]
agent_v2 = create_react_agent(model, tools_v2)
def run_agent_v2(inputs: dict) -> dict:
result = agent_v2.invoke({
"messages": [{"role": "user", "content": inputs["question"]}]
})
return result
Інструмент інформації про обліковий запис було переопределено з більш детальним описом, у якому прямо згадується статус підписки. Оскільки саме цей опис читає ШІ під час вибору інструменту, його розширення безпосередньо вирішує проблему з маршрутизацією. Агент було перебудовано з використанням оновленого інструменту та обгорнуто у нову мету run_agent_v2. Усе інше залишилося незмінним.
# Step 3: Run the evaluator on the v2 agent
experiment_results_v2 = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="agent-v2-docstring-fix",
max_concurrency=4,
)
Ідентична оцінка виконується знову на тому самому наборі даних за допомогою того ж оцінювача, причому змінюється лише цільове значення. Новий experiment_prefix дозволяє відрізнити цей запуск у інтерфейсі LangSmith, щоб його можна було порівняти з базовим варіантом. LangSmith пропускає кожен приклад через агента версії v2, зберігає кожен повний запис виконання та оцінює кожен результат за допомогою оцінювача.
Очікуваний результат:
View the evaluation results for experiment: 'agent-v2-docstring-fix-...' at:
https://smith.langchain.com/o/.../datasets/.../compare?selectedSessions=...
9/9 runs | avg tool_match: 0.89
Рейтинг підвищився з 0,33 до 0,89. Вісім із дев’яти проблем вже усунуто, а дев’та – це справді неоднозначне запитання, яке може потрапити до черги для анотації людиною.
Before (v1): After (v2):
wrong_tool: 5 wrong_tool: 1
unhelpful: 3 unhelpful: 0
human: 1 human: 0
avg score: 0.33 avg score: 0.89
Кілька факторів зробили це порівняння надійним:
- Початкова оцінка дала конкретний бал ще до будь-яких змін у коді.
- Виправлення полягало у одній точковій зміні — окремому документуванні.
- Повторна оцінка виміряла точний ефект цієї зміни на основі ретельно підібраної колекції реальних помилок.
- Набір даних для тестування залишався незмінним, тож порівняння було справедливим.
- Кожен початковий запис про помилку тепер є окремим тестом, тож жодна подальша зміна не може його потайки пошкодити.
Саме це робить процес поступовим. Кожне виправлення додає приклади до набору даних, і кожна майбутня версія оцінюється за критеріями кожної минулої помилки. Час випуску продукту з часом стає швидшим, а не повільнішим, оскільки рівень впевненості зростає разом із набором даних.
Тепер весь цикл виконується повністю від початку до кінця. Збираються дані та доповнюються оцінками, проявляються певні закономірності, збої перетворюються на тести в автономному режимі, оцінка проводиться перед випуском продукту, а зміни приймаються лише тоді, коли базовий показник покращується. Кожен цикл починається з вищого рівня.
Одне повне щоденне виконання
Один оператор може виконати весь цикл один раз у день зі свіжими даними виробництва.
# Full loop driver
def run_improvement_loop():
# 1. Pull traces
runs = list(client.list_runs(
project_name="agent-improvement-loop",
is_root=True,
start_time=datetime.utcnow() - timedelta(days=1),
))
traces = [compact_trace(r) for r in runs]
# 2. Enrich with scores
enriched = [enrich(t) for t in traces]
# 3. Find failures and categorize them
failures = [t for t in enriched if is_low_scoring(t)]
categories = Counter(categorize(t) for t in failures)
# 4. Add failures to the dataset
new_examples = [{
"inputs": {"question": t["inputs"]["messages"][0]["content"]},
"outputs": {"expected_tool": expected_tool_for_query(
t["inputs"]["messages"][0]["content"])},
"metadata": {"category": categorize(t), "source_run_id": t["run_id"]},
} for t in failures]
if new_examples:
client.create_examples(dataset_id=dataset.id, examples=new_examples)
# 5. Re-evaluate the current agent against the full dataset
result = evaluate(
run_agent_v2,
data=dataset_name,
evaluators=[tool_match_evaluator],
experiment_prefix="daily-regression",
)
return {
"traces_collected": len(traces),
"failures_found": len(failures),
"by_category": dict(categories),
"examples_added": len(new_examples),
}
print(run_improvement_loop())
Ця функція об’єднує шість етапів. Вона завантажує дані за вчора, оцінює їх, класифікує збої, додає нові збої як приклади набору даних та проводить оцінку регресії. Вона повертає невеликий словник з показниками, який можна записувати або надсилати в Slack щодня.
Очікуваний результат:
{
"traces_collected": 186,
"failures_found": 12,
"by_category": {"wrong_tool": 4, "unhelpful_answer": 6, "human_flagged": 2},
"examples_added": 12,
"regression_score": 0.87
}
Що показує результат: протягом дня було зафіксовано 186 слідів виробництва без змін у коді агента, причому дванадцять з них мали принаймні один низький рейтинг. Тепер кількість не корисних відповідей перевищує кількість неправильних викликів інструментів, що свідчить про ефективність виправлень, пов’язаних із маршрутизацією, та про те, що наступні виправлення потрібно здійснити у процесі генерації відповідей. Рейтинг регресії залишився на рівні 0,87, тож жодна з змін тижня не призвела до повторення попередніх проблем. Дванадцять нових прикладів додалися до постійного набору даних, де вони будуть запобігати їх повторенню.
П’ять оновлень, які варто здійснити найближчим часом
Кожне з цих оновлень стосується функцій LangChain, LangGraph чи LangSmith, які можна активувати без необхідності перебудови всієї архітектури.
- Замініть класифікатор на основі регулярних виразів на ембеддинги.
OpenAIEmbeddingsможе створювати ембеддинги для кожного запиту, тож сліди невдач групуються за семантичною схожістю, а не за правилами ключових слів, що дозволяє виявити більш тонкі проблеми, такі як неузгодженість тону. - Замініть вбудований суддю LLM на бібліотеку OpenEvals. У швидкому старті LangSmith використовується
openevalsіз готовими запитами, такими якCORRECTNESS_PROMPT, який працює краще, ніж самостійно створений суддя, та має варіанти без посилань для онлайн-моніторингу. - Додайте оцінку кількох раундів спілкування. Функція багатораундової оцінки LangSmith запускає суддю LLM на всю довжину розмови, а не лише на окремі відповіді, що дозволяє виявити зміни в логіці міркувань, які залишаються непоміченими при однораундовій оцінці в агентах на основі чату.
LANGSMITH_PROJECT=agent-canary протягом дня, а потім порівнюючи її результати з проектом-контролем, можна переконатися, що вона замінить продакшн-версію лише у разі суттєвої кращості.Потім цей цикл щоразу повторюється з вищою вихідною точкою, з кращими треками, кращою оцінкою та кращим агентом.