Фінансавы агент для QuickBooks на лакальнай LLM з Ollama і LangChain
ReAct-агент на Qwen 3.5 9B і RTX 4070 чытае жывыя даныя QuickBooks праз LangChain і Ollama — лакальная LLM без выдаткаў на воблака. Код, вынікі і абмежаванні.
Крыніцы: **Building an Agentic AI App With a Local LLM — No Cloud, No API Costs**
Ёсць асістэнт па фінансах, які падключаецца да QuickBooks Online, запоўнюецца рэальнымі данымі падпрыемства і дазваляе власніку співацца з ШЫ пра фактычныя показнікі: рэальныя квітанцыі, рэальны потокі грошаў, рэальная структура дзейнароў, і ўсё гэта — не дамовыя даны. Мадэль працюе абоўсумова на ноутбуку. NVIDIA GeForce RTX 4070 з Qwen 3.5 9B, які працюе праз Ollama, выкаанаўвае всі вычысленні, таму пад час розработкі няма вызоўаў да OpenAI, Anthropic, выкаанавання ў воблаку і няма рахунку за ШЫ.
У наступным чытанні практычна паказаны рэальны код, выборы архітэктуры і тое, што стварае асістэнт, а не тэорыя.
Проблема, якую ён рашае
Власнік малага падпрыемства ў QuickBooks мае всі фінансавыя даны ў адным месцы, але для таго, каб ператворыць іх у корысную інфармацыю, патрэбны альбо навыкі бухгалтэра, альбо платныя професыйныя послугі. Стандартныя справаздачы паказваюць, што адбылося. Але яны не кажуць, што робіць далей.
Мэтаем было дазволіць чалавеку запісаць просты англійскі запіт пра своія грошы і атрымець конкрэтную, падтверджаную данымі адпаведнасць: номеры рахункоў-фактур, імены кліянтаў і рэальны стан грошаў, а не загальні рады.
Архітэктура
Сістэма складаецца з трох слоёў:
- Фронтэнд: Next.js 14 з TypeScript і shadcn/ui
- Бэкэнд: FastAPI, PostgreSQL і SQLAlchemy
- Штучны інтэлект: агент LangChain ReAct, Qwen 3.5 9B праз Ollama і набор засобаў QuickBooks
Найважлівейшым рашэннем было стварэнне шара штучнага інтэлекту на францызскай рамцы агента LangChain. Ён дазволяе модэлю самам выбраць, якія даныя запрашаць пры адпаведзе, і самэ гэта ўрабляе тое, што адпаведзі выходзяць конкрэтнымі, а не загальнамі.
Работа на GPU ноутбука
Весь шар штучнага інтэлекту работае на GPU спожывачскага ноутбука — RTX 4070 з 8 GB VRAM. Значэнні, якія былі замераны, паказаны ніжчэй.
Адказ даецца прыблізна за адзін мінут. Для фінансавага аналізу гэта прыемна трываласць, аджэ людзі, якія ставяць сутэчныя запытанні, спакульваюць рэштычнасць, а не простыя размовы з чат-ботамі. У практычных умовах, калі абстракцыю LangChain наставіць на паводарніка інферэнсу ў воблаку, трываласць адказа зменшыцца да пяці секунд, а код прыкладнага програму не будзе зменены.
Тры прычыны зробілі локальную рэалізацыю правым выборам для разработкі:
- Гэта не коштуе нічога. Кожная змяна запытання, сесія дэбагавання і тэсты на крайнія случаўныя сітуацыі выконваюцца без вітатак і без лімітавання частоты.
- Гэта захоўвае прыватнасць дадзеных. Даныя з QuickBooks ніколі не выходзіць за межы апарату, таму ні сума рахунку, ні імя кліента, ні показнік грошовага аблыку не потрапляюць да сторонніх паводарнікаў AI.
- Гэта падтверджае тэзу. Якшо мадэль з 9 мільярда параметраў на спожывачскай GPU можа даць такую ж якасць рэзультатаў, як і тыя, што паказаны пазней, то стандарты апаратуры для стварэння локальных прыкладных програм на AI ніжэй, чым думае багато распрацоўшчыкаў.
Пакалькі Лангчэйн знаходзіцца ў сэрэдзіне, таму працэс разработкі і експлуатацыі выкарыстоўвае аднойчыны кодавую базу. Змена бэкенда для вырахунка рэзультатаў трывае два радкі коду:
Інструменты, памяць, логіка агента і запіты застаюцца тымі ж. Мадэль ёсць толькі дэталлю рэалізацыі.
Агент
Шар AI выкарыстоўвае фрэймворк агента ReAct ад Лангчэйн. Агент мае набор інструментоў, якія ўособліваюцца простымі функцыямі на Python, якія запошчаюць актуальныя даны з QuickBooks, і выбірае, калкі з іх запусціць, адпаведна да запіту карыстальніка. Гэта ўсё налаштаванне з файла agent.py:
from langchain_ollama import ChatOllama
from langchain_core.tools import tool
from langchain.agents import create_agent
from langchain_core.runnables import RunnableConfig
from quickbooks_client import fetch_invoices, fetch_profit_loss, \
fetch_cash_flow_summary, fetch_ar_aging
llm = ChatOllama(
model='qwen3.5:9b',
base_url='http://127.0.0.1:11434'
)
@tool
def get_invoices(config: RunnableConfig) -> str:
"""Fetch all invoices from QuickBooks including amounts and balances."""
user_id = config.get('configurable', {}).get('user_id')
return json.dumps(fetch_invoices(user_id=user_id), default=str)
@tool
def get_ar_aging(config: RunnableConfig) -> str:
"""Get AR aging report with buckets and DSO calculation."""
user_id = config.get('configurable', {}).get('user_id')
return json.dumps(fetch_ar_aging(user_id=user_id), default=str)
@tool
def draft_collection_notice(customer_name: str, amount: float,
days_overdue: int) -> str:
"""Drafts a reminder for an overdue invoice based on severity."""
if days_overdue > 60:
tone = 'Firm & Urgent'
msg = f'Final reminder: your balance of ${amount} is significantly overdue.'
elif days_overdue > 30:
tone = 'Standard Follow-up'
msg = f'Checking on your ${amount} payment, now 30+ days past due.'
else:
tone = 'Friendly Reminder'
msg = f'Quick note: your payment of ${amount} was due recently.'
return json.dumps({'tone': tone, 'draft': msg})
tools = [get_invoices, get_profit_loss, get_cash_flow_summary,
get_ar_aging, draft_collection_email, draft_collection_notice]
agent = create_agent(
model=llm,
tools=tools,
system_prompt=(
'You are an AI CFO assistant for small businesses. '
'Analyze financial data and give clear, actionable advice. '
'Always highlight cash flow risks and overdue invoices. '
'Be concise and specific with numbers.'
'\n\n--- COLLECTIONS SPECIALIZATION ---'
'\nWhen asked about unpaid invoices or specific customers:'
'\n1. Use get_ar_aging to identify debt seniority.'
'\n2. Use draft_collection_notice to generate professional follow-ups.'
'\n3. Explain the impact on liquidity, not just the invoice.'
)
)
Два моменты заслуговуюць на увагу. Перша — user_id пераводзіцца праз кожны вызов інструмента праз RunnableConfig з LangChain, і самэ гэта робіць сістэму мнагатэнантной: агент кожнага карыстальніка працюе толькі з дадзеннямі QuickBooks цаго самога карыстальніка. Другая — агент фактычна чытае докстрынгі інструментаў пад час адлікавання, калі што запускаць, таму іх трэба пісаць як інструкцыі для мадэлі, а не як апісанні для людзей.
Історыя размовы
Кожна сесія зберагае свою сабэйную історыю, таму наступныя запітанні можаць апыляцца на паказаны ранейшыя крокі:
conversation_history: dict[str, list] = {}
def ask(question: str, session_id: str = 'default',
user_id: str = None) -> str:
if session_id not in conversation_history:
conversation_history[session_id] = []
conversation_history[session_id].append(
{'role': 'user', 'content': question}
)
config = {'configurable': {
'user_id': user_id,
'session_id': session_id
}}
response = agent.invoke(
{'messages': conversation_history[session_id]},
config=config
)
answer = response['messages'][-1].content
conversation_history[session_id].append(
{'role': 'assistant', 'content': answer}
)
return answer
Вівок можа запітаты: „Што гэта за DSO?“ і потым „Які кліенты гэта спрычыняюць?“, і агент розуміе, што маецца на увазі під „цым“. Історыя запам’ятовуецца за допомогою session_id, тому одночасні корыстувачы маюць окрэмі розмові.
Кінцева точка чату
Кінцева точка FastAPI за чатом ёсць простою. Ключовій момент полягае у тому, што user_id походыць зіснуваного JWT, а не з тыла запітування, што заважае будзь-кому отрыматы доступ до даных іншого корыстувача шляком рэдагування параметрах.
@app.post('/ask')
async def ask_agent(
request: AskRequest,
current_user: User = Depends(get_current_user)
):
# Always use the verified user from JWT, never trust the request body
user_id = str(current_user.id)
answer = ask(
question=request.question,
session_id=request.session_id,
user_id=user_id
)
return {
'question': request.question,
'answer': answer,
'session_id': request.session_id
}
Адкантэрацыя токенаў
Кліент QuickBooks адкантэруе токены OAuth автаматычна. Яны выгараюць пасля 60 хвілін, таму кліент перакантруець вяк токена пры кожнай запытке і адновіць яго, калі гэта неабходна:
def get_qb_client(user_id: str = None):
db = SessionLocal()
qb_token = db.query(QBToken).filter(
QBToken.user_id == uuid.UUID(user_id)
).first()
if not qb_token:
raise Exception(f'No QB token found for user {user_id}')
auth_client = AuthClient(
client_id=os.getenv('QB_CLIENT_ID'),
client_secret=os.getenv('QB_CLIENT_SECRET'),
redirect_uri=os.getenv('QB_REDIRECT_URI'),
environment=os.getenv('QB_ENVIRONMENT'),
access_token=qb_token.access_token,
refresh_token=qb_token.refresh_token,
)
# Only refresh if token is older than 55 minutes
token_age = datetime.utcnow() - qb_token.updated_at
if token_age > timedelta(minutes=55):
auth_client.refresh()
qb_token.access_token = auth_client.access_token
qb_token.refresh_token = auth_client.refresh_token
qb_token.updated_at = datetime.utcnow()
db.commit()
return QuickBooks(
auth_client=auth_client,
refresh_token=auth_client.refresh_token,
company_id=qb_token.realm_id,
minorversion=75
)
Адынчае апдэты наступаюць за 55 хвілін уместо 60, што дапамагае прабачыць некальканаследжванне часу. Апдэты выкарыстоўваюцца на стороне сервера, а токен зберагаецца ў PostgreSQL, а не на фронтэндзе.
Што генеруе агент
Гэта і частка, якая мае значэнне. Ёсць рэальны выхід для запіту "Калі мой DSO і чы гэта становіць проблэму?", выконанага на даных QuickBooks sandbox:
Нічога тут не ўніверсальнае. Агент вызваў get_ar_aging, отрымаў актуальныя даны і працаваў з ямі. У адпаведзі наводзяцца імены клёянтаў, ацэнюецца вплыв затрымкі на грошовы поток і пропонуецца стварэнне павестака пра выеўку. Агент можа гэта зробіць, таму што існуе інструмент draft_collection_notice, і агент ведае, калі ўжо можна яго выкарыстоўваць.
Чаму гэта не просты бот RAG
Тое, што адзінакоўвае яго ад чат-бота для пошуку інфармацыі, — гэта тое, што агент сам выбірае свае інструменты. Калі яго запытаюць: «Кальво маёе большое рызыкі ў грошовых праймутах?», ён не запускае всі працэсы. Ён аналізуе запітанне і выбірае стосунечныя інструменты. Цыкл ReAct выглядае так:
Для гэтага запытання ён прыхоўчуе get_invoices і get_profit_loss, таму што яны неякісны. Такая селектыўнасць дапамагае заставіць адпаведзі цярпкімі і не дазволяе вікна кантэксту заполняцца непатрэбнымі дадзеннямі.
Панель керування
Панель керування выкарыстоўвае тыя ж інструменты QuickBooks, якія вжывае агент. Кожны показнік ёсць актуальным, так як межы корыстувача і QuickBooks нямае шара кэшавання, таму графік вякавання і спіс самых затрымлёных рахункоў апдэйтуюцца з кожным завантажэнням стороніцы.
Старэнчныя даны з AR пераканальваюць за межы стандартнага адчытнага справаздачы QuickBooks. Ён на месцы вырахоўвае показнік DSO, прызначае рэгламент рызыку кожнай групе і паказвае кожную вяроцэнку з точным лікам дзён запазджэння. Цыфры падаюць з той самай функціі fetch_ar_aging, яку вызывае агент, што забезпечвае едынакавасць даных.
Рашэнняя праектавання, якія варта адзначыць
- Толькі чытанне за дапамою OAuth. Запрасжаны дыяпазон не мае прав на збіранне дадзэнняў, таму аплікацыя структурна не можа зменіць даны QuickBooks.
user_idз JWT. Канцэнтры/askпаверае перакананы токен, а не што-небудзь іншае, што надае кліент.- Няма жодных фінансавых дадзэнняў, якія бы былі збераганы. Цыфры QuickBooks запрашваюцца толькі калі ў тым патрэба, а пасля — выкарыстоўваюцца і збрасваюцца. У базе дадзэнняя застаюцься толькі імены выкарыстоўначаў, токены і расклады.
- Пад час стварэння вікорыстоўваецца локальны LLM. Жадных фінансавых дадзэнняў не надаўалася жоднаму зовнішнему прадаўцу AI пад час стварэння і тэставання.
Практычныя перавагі выкарыстоўвання раходных рашэнняў у местыях у параваненні з падходам, які прыорытэтнаў бы хмарныя сервісы:
Косцом являецца затрымка — адпаведна 60 секундаў з выкарыстоўванням карта 4070 протыява менш чым 5 секундаў з выкарыстоўванням хмарнага API. Для інструменту, якім павінны корыствацца людзі, якія хочуць адпаведных адказоў, гэта прыемна пад час розработкі і тэставання, а пераход на хмарны мадэль для прыемнай експлуатаціі ёсць даскладным.
Сколькі часу цэе зайняло
Работа, якая зазвычай займае калькі недзеў, была скорачана да калькі дней. Апрыбліжны графік для адзінаго разрабоўца, які выкарыстоўвае дапамогу AI у напісанні коду працоўна:
Асалевы сістэма запрацавала пасля трох-чатыро дзён, плюс ўжо адны дзень на доўнейшую наладку і тэставанне. Найбольш часу забралі тры прычыны: недагэдзінства ў API QuickBooks (фармат справаздачы пра старэнне дадзеных зовсім не інтуўітывны), наладка агента LangChain (опісы інструментаў маюць большое значэнне, чым можна было спадзявацца), а таксама правільная перадача значэння user_id праз кожны вызов інструмента для падтрымкі мнагакліентскай архітэктуры. Швальнасць значную частку даў вбудоўаны механізм верыфікацыі FastAPI, абстракцыя падаўчыка ў LangChain, а таксама дапамога AI у напісанні коду, адлучэнні бягаў і рашэнні пытанняў, зв’язаных з архітэктурай.
Што трэба змяніць
Пад час другога прабавы калькі прыемы будуць адбывацца інакш:
- Інструменты і агент трэба было бы з’еднаць ўжо з першага дзёну. Слой дадзеных быў завершаны першы, а пасля — падключаны да агента, таму несупараднасці інтэрфейса сталі з’яўляцца пазней.
Відомі обмежэння
У будзь-якому чэсному опісі мае буты зазначэно, што залішылося недоробленім. Ось што стрымуе поточну версію ад масштабування і як можна подолаты кожну з цых проблем.
Історыя в памяці
Історыя розмов зберыгаецца у звічайному словніку Python у оператывній памяці:
Гэта прыемна для адзінаго сервера развіцця, але мае тры проблемы ў рэжыме продакшну. Історыя знікае пасля кожнага перзапуску. Ён не можа працаваць на калькі серверных інстансаў, што выключае можлівасць горызонтальнага масштабавання. Кроме тыго, нічога не застаёцца между сесіямі, таму закрыцце браузера завершае вярсаванне.
Рашэння ў рэжыме продакшну — перанесці історыю ў Redis для шырокага чытання і запісу з тайм-аутам, або ў PostgreSQL для полной стабільнасці дадзеных. Абстракцыі історыі паведамленняў у LangChain падтрымляюць оба варыянты, таму змена ўсё проста.
Без RAG, спецыяльна
Сістэма не выкарыстоўвае ніякога адзысквання дадзэнняў: ніякіх эмбедынгаў, ніякой вектарной базы дадзэнняў, ніякога працэсу разбівання на часткі. Гэта зраблена намерна. Фінансавыя дадзэнні постаўляюцца стала — плацяцься рахункі-фактуры, з’яўляюцься новыя, а балансы зменяюцца, таму эмбедынгі, створаныя вчора, сёння можу быць застарэлымі. Адзыскванне актуальных дадзэнняў з QuickBooks па кожным запіту гарантуе, што адпаведзі будуць адпраўляцца з учыткам рэальнага стану дэла.
RAG падходзіць для статычных або мала зменяючыхся матэрыялаў, такіх як контракты, правілы і історычныя звярненні. Для актуальных транзакцыйных дадзэнняў прымусовыя вызывы інструментаў даходзяць да большай точнасі, чым адзыскванне дадзэнняў праз эмбедынгі.
Цена завісіць ад часу адзысквання і колькасці токэнаў, адтак як новыя запыты выкарыстоўваюць больш часу і заполняюць больш часткі кантэкста, чым кампактныя апісанні з вектарной базы дадзэнняў. Працуючы з дзесяткамі або каля сотні рахункаў-фактураў на месяц, гэта ўзяць можна. Пры большых обсягах доцэльны гібрыдны падход — RAG для історычных дадзэнняў і прымусовыя інструменты для актуальных дадзэнняў.
Вялыя кантэксты счытанняў
Зараз усе счытанні прыходзяць у цэлым выразе да агента. З 30-50 счытаннямі такі падход працюе добра. Калі ж іх тыячы, выкананы ліміты токенав і падыходзіць якосьць. Рашэннем ёсць фільтрацыя прытаманнае да таго, каб модэль не бачыла усё: трэба запрашваць толькі тыя счытанні, якія стосуюцца запытання, спачатку падсумавляць вялыя наборы дадзэнняў, або вікорыстоўваць два этапы, калі на першым этапе швыткае запрашэнне скурчвае кантэкст прытаманнае да рассуждэнняў.
Ёшча без стрымінгу
Сервер вяртае весь адказ заодно, таму на локальным GPU корыстувальнікі мусяць чакаць прыблізна хвіліну, пакуль не пакажыцца порожній экран. FastAPI адказвае памкой StreamingResponse, Ollama можа стрымаваць токены праз свой API, а агенты LangChain падтрымляюць стрымінгавыя калебы. Іх з’яеднанне проста, але яго адклалі, каб спачатку запрацавалі основныя функцыі, і гэта ёсць прыорытэтам пры запуске.
Сесіі, якія не трываюць
session_id ствараецца на фронтэндзе, калі завантажаецца сторанка чату. Нявярожанне новай карточкі, апдэйт або вярнуцца пасля закрыцья браузера ствараець новы session_id, і історыя чату пачынаецца занова, нават калі сервер ніколі не перэзапускаўся. Фінансавы асістэнт могаў бы значна выгадаць ад стойкага запамятавання — напрыклад, калі бы мог ўспамінаць, што кліент выкарыстоўвае план адплатаў або што рахунак-фактура быў обгавароўваны ў празылець. Правільнае выкарыстоўванне такога запамятавання значыць зберагчы інформацыю пра сесіі ў базе дадзенаў і дазволіць корыстувачам называць, прызначыць да продажу і выдаліць практычнае спілкування з працэсамі.
Вывад
Абстракцыя LangChain, локальныя вычысленні Ollama і доступ да дадзенаў QuickBooks разам даюць ўсё, што ўжо корыстней, чым кожны з эых элементаў окаласці. Агент не проста запоўнюе інформацыю — ён аналізуе яе, выбірае тое, што мае значэнне, і дае адпаведныя адказы, якія стосуюцца фінансавых справ кожнага конкрэтнага корыстувача.
Работа на місцевым рынку — гэта не компроміс. Гэта легітмовая стратэгія развіцця, якая дае рэальныя перавагі ў плане прыватнасці, віткоў і можлівасцяў постаўкі новых версый. Апаратныя прыстроі для корыстувачаў, такія як RTX 4070, здатны запускаць мадэлі размерам 9 мільярдаў параметраў. Для тых, хто плануе стварыць дапыт на базе штучнага інтэлекту, лепшы вопыт — не які хмарны прадаўца выбраць, а чы можна запускаць потрэбныя мадэлі на місцы пад час розработы, каб продукт могаў быць перакананы без жадных віткоў на апрацоўку даных, а рашэння пра прадаўця хмарных послуг можна было вырашыць толькі пасля таго, як будуць у наявнасці рэальныя даны пра ўжытак. На RTX 4070 адказ — так.