Локальны ўпраднік AI з Ollama: спецыялізаваныя інструменты, парадактывацыя і строгія ліміты колькасці крокаў
Адміністратыўны агент AI на Ollama, які працуе за прынцыпамі: скураныя спісы дапускаемых інструментаў, обмежаны цыкл, структураваны выходны дадзеныя на аснове Pydantic, чыста выражаны стан і набор тэстоў для пераканання ў правільнасці работы.
Крыніцы: I Built a Local AI Agent With Ollama — and the Hard Part Wasn’t the Model
Першая версія агента для стварэння местных рахунков-фактур выглядае вражаюча прыблызна пяць хвілін. Задача простая: дапамогчы власніку малага бізнесу пераследаваць праслужылыя рахункі-фактуры. Агент можа шукаць кліентаў, знаходзіць неплатыяныя рахункі-фактуры, вырахоўваць, насколькі воны запаздалі, і складаць нагадванне. У тэставанні яго прасілі знайсці праслужылыя рахункі-фактуры для компаніі Acme Plumbing і падготавіць дружэльнае нагадванне. Ён адразу ж адпаведзеў з рахунком-фактурой, залішкам і апранутым электронным лістам.
Проблема ў тым, што частка інфармацыі ў цім ўявная. Кліент існуе, але номера рахункова-фактурнага документа няма. Сума здаецца правдападобной, але ў яе ёсць памылка. Агент стварыў візуальную форму успішнага рэзультата, не выканавшы рэальной работы.
Этый эпізод выкладвае найважнейшы урок пра стварэнне агентаў: тое, што чалавек звучыць розумна, не значыць, што ён надзеяны. Агент ёсць надзеяным, калі кожна значныя тверджэнне мае падставу ў кантролюемай дзеянні або надзеяным істочніку. Далейша частка перабудоввае агента для стварэння рачунка на базе Ollama і Python, дадаючы явныя інструменты, механізмы верыфікацыі і компактны цыкл агента. Аналіз даступны на локальным камп’ютары, але этыя урокі прыменяюцца практычна да будзь-якай парадыгмы агентаў.
Чаму Ollama
Локальная наладка адмовяецца ад адправкі дакументаў кліентаў да зовнішньага прадаўца модэляў падчас разработкі, а таксама дазволяе змінюваць модэлі без перабудовы прыкладнення. Ollama запускае модэлі локальна праз просты API для чату. Таксама падтрымліваецца вызыванне інструментаў, таму модэль можа запрашаць функцыю замест таго, каб выдаваць фальшывыя адпаведзі. Наладка адбываецца за лічыць секундаў:
ollama pull qwen3
pip install ollama
Звычны ўпыт чату з Python выглядае так:
from ollama import chat
response = chat(
model="qwen3",
messages=[
{"role": "user", "content": "Explain what an overdue invoice is."}
],
)print(response.message.content)
Гэта дае чат-бота, а не агента.
Чат-бот адказвае, агент дзеяе паэтапна
Слова «агент» выкарыстоўваецца без строгай дэфініцыі. Практычная дэфініцыя — гэта модэль, якая працуе ў цікле, маея доступ да інструментаў, стану і правілаў, якія кантролююць тое, што яна можа зрабіць далей. Для задачы з рахункам-фактурой цікл выглядае так:
- Пользоватэль выдвайае запіт.
- Модэль выявляе, чы трэба інструмент.
- Програма парабягае запіт на інструмент.
- Програма запускае інструмент.
- Рэзультат дадаецца да размовы.
- Модэль запрашвае іншы інструмент або дае заканчыцкі адказ.
Мадэль ніколі не практыкуецца да базы дадзеных. Яна можа толькі прабавіцца прабачыць прыемліку вызваць інструмент, які задаў разрабоўка. Гэтыя межы маюць значэнне, таму што мовныя мадэлі добра справляюцца з адгукам на намеры і стварэннем тэксту, тады як код прыемліка лепшая падходзіць для керавання правамі, запытамі, вычысленнямі і бізнес-правіламі.
Спачатку — простыя інструменты
Паўзучая памылка — гэта наявнасць аднаго моцнага інструмента, напрыклад manage_invoices, які можа шукаць, адкладзіць змяны, ствараць праекты і адправляць інформацыю. Гэта ускладняе дэбаггін і робіць неправільнае выкарыстоўванне інструмента апасным. Лепшая працуюць маленькія інструменты, кожны з якіх віконвае адну задачу:
find_customerператварае імя кліента на рэальны ID кліента.get_overdue_invoicesвяртае праекты рахункоў, якія застаўся неаплацаванымі, для таго ID.draft_reminderстварае тэкст, але нічога не адправляе.send_reminderвыкананаў зовнішнюе дзеянне і патрабуе затверджэння.
Спроставаны локальны слой дадзейна падае факты:
CUSTOMERS = {
"acme plumbing": {
"customer_id": "cus_1042",
"name": "Acme Plumbing",
"email": "billing@example.com",
}
}
INVOICES = [
{
"invoice_id": "INV-2048",
"customer_id": "cus_1042",
"amount": 1850.00,
"days_overdue": 18,
}
]
def find_customer(name: str) -> dict:
customer = CUSTOMERS.get(name.strip().lower())
return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
matches = [
invoice
for invoice in INVOICES
if invoice["customer_id"] == customer_id
and invoice["days_overdue"] > 0
]
return {"invoices": matches, "count": len(matches)}
Паглядзіце, чаго не робяць гэтыя функцыі. Яны ніколі не здагадваюцца. Апавядзенны кліент стварае чыткую памылку, а калі няма адпаведных рахункаў-фактур, то ствараецца порожняя спіс.
Рэальныя інструменты, а не выдуманы доступ
Ollama прыймае функцыі на Python як інструменты, а іх назвы, параметры і докстрынгі даюць модэлю тое, што яму трэба, каб выбраць правільную функцыю.
import json
from ollama import chat
def find_customer(name: str) -> dict:
"""Find a customer by business name and return its verified record."""
customer = CUSTOMERS.get(name.strip().lower())
return customer or {"error": "customer_not_found"}
def get_overdue_invoices(customer_id: str) -> dict:
"""Return overdue invoices for a verified customer ID."""
matches = [
invoice
for invoice in INVOICES
if invoice["customer_id"] == customer_id
and invoice["days_overdue"] > 0
]
return {"invoices": matches, "count": len(matches)}
TOOLS = {
"find_customer": find_customer,
"get_overdue_invoices": get_overdue_invoices,
}
Слоўнік рэестру — гэта не проста зручнасць, а ўсё-такі спіс дозволеных функцый. Нават калі мадэль просіць функцыю пад назвай delete_customer, прыкладнэе рашчытае гэта запітанне, таму што такога інструмента няма ў TOOLS.
Цыкл агента
Цыкл і ёсць тым, што ператварае вызыв інструментаў у агента. Ён пачынаецца з прапановы сістэмы:
SYSTEM_PROMPT = """
You are an invoice assistant.
Rules:
- Never invent a customer, invoice, email address, balance, or date.
- Use find_customer before requesting invoices.
- Only use customer IDs returned by tools.
- If a tool returns an error or no records, explain that clearly.
- You may draft communication, but you cannot send it.
"""
def run_agent(user_request: str) -> str:
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_request},
] for _ in range(6):
response = chat(
model="qwen3",
messages=messages,
tools=list(TOOLS.values()),
) messages.append(response.message) if not response.message.tool_calls:
return response.message.content for call in response.message.tool_calls:
name = call.function.name
arguments = call.function.arguments if name not in TOOLS:
result = {"error": "tool_not_allowed"}
else:
try:
result = TOOLS[name](**arguments)
except (TypeError, ValueError) as error:
result = {
"error": "invalid_tool_arguments",
"detail": str(error),
} messages.append(
{
"role": "tool",
"tool_name": name,
"content": json.dumps(result),
}
) return "I stopped because the task exceeded the maximum number of steps."
Ліміт у шасаціх ітэрацый ўзначаны намеравана, таму што агент ніколі не павінен працаваць без канца, перэканаляючы тую ж проблему. Гэты цыкл таксама спрытвае выход з адзінаго інструмента як свежыя доказы. Мадэль спачатку разв’язвае запіт на імя „Acme Plumbing“, отрымае cus_1042 і толькі пасля чаго запрашвае рахункі-фактуры для гэтага падтверджанага ідэнтыфікатора.
Лепшы запіт не быў рашэннем
Галюцінацыі падказваюць распрацоўшчыку дадаць ў сістэмны запіт ўсё больш адну рэчэнцю, напрыклад, просьбу быць точным і ніколі нічога не выдумваць. Такія інструкцыі дапамагаюць, але не ўсунулі проблему. Надзеянае рашэнне прыйшло ад пераструктуравання:
- База дадзеных застаецца аднаго істочнікам правды.
- Мадэль можа запрашваць даныя і не можа выдумаць запіс, які будзе вважацца правамі.
- Імены інструментоў знаходзяцца у спіску, якія дазволены, а аргументы пераверачваюцца.
- У цыкле ёсць жорсткі ліміт колькасці крокаў.
- Чытанне і запіс застаюцца аддзеленымі.
Папярэджэнні вплываюць на паведамленне, а код гарантуе яго выкананне.
Структураваны выхід на межы
Тэкст у вольнай форме падходзіць для дружэльных электранайтов, але ён ўпадзячы, калі іншая частка прыкладнення выкарыстоўвае адпаведны адпаведзень. Калі агент павінен вернуць экран з адгукамі кліента, ідэнтыфікаторамі рахункоў, загальным залішкам і праектам, модэль Pydantic можа задаць такі контракт:
from pydantic import BaseModel, Field
class ReminderReview(BaseModel):
customer_name: str
invoice_ids: list[str]
total_due: float = Field(ge=0)
draft_subject: str
draft_body: str
requires_approval: bool = True
Ollama прыймае схему JSON праз параметр format:
review_response = chat(
model="qwen3",
messages=messages,
format=ReminderReview.model_json_schema(),
)
review = ReminderReview.model_validate_json(
review_response.message.content
)
Гэта гарантуюе, што адказ будзе мець апэктыўваную форму. Аднак гэта не падтверджвае, што значэнні ўсё правільныя, таму invoice_ids і total_due яшчэ парабягаюцца з рэальнымі рэзультатамі апарату прычымо да таго, калі з’явіцца кнапка затварэння. Паўеракцыя схемы перакладваецца, чы рэч мае правільную форму. Бізнес-паўеракцыя перакладваецца, чы рэч дазволена і правільна. Агенту для працы трэба і тое, і другое.
Стан — гэта не памяць
Дзялёныя размовы выяўляюць ўжо адну проблему: модэль забывае, калькі кліента быў выбраны, або спаўнае стары рахунак у новы запіт. Прычына — адносленне да всей транскрыпцыі чату як да стану прыкладнення. Размова ёсць корыстным кантэкстам, але крытычны стан павінен знаходзіцца ў чыстае структуры:
task_state = {
"customer_id": "cus_1042",
"verified_invoice_ids": ["INV-2048"],
"approved_actions": [],
}
Мадэль можа ствараць аптаты з выпадакоў, але прыкладнэе мае паверыяныя ідэнтыфікаторы і затверджэнні. Калі размова скорачваецца, каб паспасціла ў вікна кантэксту, гэтыя факты застаюцца.
Дзейснае дзейство таксама вплывае на выканання, адколькі большы кантэкст выкарыстоўвае больш памяці і трэба больш часу на апрацоўку. У змену таму, каб безканцэсантна адправляць кожную паведамленне і радок базы дадзенаў, неабходна перадаць самы мінімальны набор мяркаваў, якія трэбуюцься для дакладнай дзеяздзе.
Локальнае не значыць безпечнае
Адкліканне модэлі праз Ollama дазволяе контролаваць працэс аналізу з локальнага розмесця, але не захоўвае безпеку рэшты прыкладнэга. Агент все ўсё можа выцягнуць даныя ў логі, вызваць небезпечны внутршні канецпункт, следаваць зловучым інструкцыям, спрывучытым у дакуменце, або дзеяць з надмернымі правамі. Праўільныя прынцыпы безпекі бэкэнду застаюцься актуальнымі:
- Давайце кожнаму інструменту толькі тыя правыя, якія яму трэбуюць.
- Утрымвайце секрэты праза паведамленні і выкладчык коду.
- Спрытаваны тэкст трэба спрытваць як ненадзеяныя даны, ніколі не як інструкцыі сістэмы.
- Запісваць кожны вызов інструмента разам з яго рэзультатам, часам адклікання і заканчэчным рашэннем.
- Удаліць чутлівыя поля з рэзультатаў прытым, як тыя толькі прабіваюць прайсці ў кантэкст модэлі.
- Трэба атрымваць разрэшэнне на зовнішнія або незворачныя дзеянні.
- Зробіць важлівыя запісы ідэмпатнымі, каб пракаты не стваралі ўдвойчыных копій.
Модэль ніколі не павінна быць межой безпекі.
Тэставанне
Дэманстрацыі успішнага падчынення маскуюць большасць неудач агента, таму варта стварыць невялікі набор для ацэнкі. У цым наборы ёсць прыклады вядомага кліента з аднай запаздзелай рачункай і ўтолькі без яе, імена, якія не падходзяць нікому, два схожыя назвы компаній, спроба адправкі без параджэння на затверджэння, інструкцыя пра тое, каб заняхаміць правіла, некорэктна настройка параметра інструмента і інструмент, які выканае тайма-аут. Для кожнага з прыкладаў неабходна запісаць апэктыўныя інструменты, дазволеныя дзеянні і канечны рэзультат, а таксама перзапускаты весь набор кожны раз, калі змянююцца модэль, запрошэнне сістэмы чыстая апісанне інструментаў.
Найболей корыстным показнікам не ёсць тое, чы гучна звучыць канечная адпаведзь. Гэты показнік — чы агент выконаў правыя, перакананыя крокі.
Рабочая версія
Пасля перадзяленае первісны запрос заставляе след такі:
User request
→ find_customer(name="Acme Plumbing")
→ verified customer_id: cus_1042
→ get_overdue_invoices(customer_id="cus_1042")
→ verified invoice: INV-2048, $1,850, 18 days overdue
→ generate draft
→ wait for human approval
Гэта версія менш «чароўная», чым першая дамаверка, але набліжнае болей практычная. Неіснуючы кліент змушвае агента зупініцца і прабаваць выясніць дэталі. Жадных прастаўзначаных рахункоў-фактур не фіксуецца як такіх. Запрос аб невядомым інструменце адхіляецца прыемнікам, а цыкл, які працуе занадта даваў, безпечна зупініцца. Нічага з гэтага няѐе вялікай красы, але ўсё гэта спрыяе падзяржанню довер'я.
Апошні урок
Ollama спрыяе простам запуску супэльнага локальнага модэлю, але модэль — толькі адна з частак. Праўый агент — гэта сістэма, якая яго абрамляе: цыклы, контракты інструментаў, даныя, якія ўтвараюць адпаведнасць правды, шар верыфікацыі, машына станоў, межы праваў і этап затверджэння.
Для пачатківца лепшая адзінця не вікорыстоўваць дзесяць інструментоў і повны доступ да прыкладнення. Пачніце з адной рэальнай проблемы і двух інструментоў з правамі толькі на чытанне, фіксуйце кожную рашэнне, дадзіце прыклады няудач, і даўце права на збіранне толькі тады, калі спосаб чытання будзе абсалютна надзеяны. Важлівая змяна — гэта замена пытання пра тое, як зробіць мадэль болей самастоятэльной, на пытання пра тое, як зробіць кожны крок відным, пераканальным і безпечным. Самастоятэльнае працаванне ўтварае хорашую дэманстрацыю, а кантролюемая самастоятэльнасць — корыстны продукт.
Супаўзвязаныя матэрыялы
- Цікл локальнага агента для кодавання: Aider, Ollama, Git worktrees і просты спосаб вярнення да пачатковага стану — Абмежаны рабочы працэс для локальнага агента для кодавання, які дазволяе пераглядаць і вярнуць кожную змяну, з выкарыстоўванням Aider, Ollama, Git worktree, спецыяльных тэстаў і простаг спосабу вярнення.