1. 为什么我要把记账 Agent 拆成 Harness 骨架
个人财务记账这件事,痛点从来不是「不会写代码」,而是「每次想认真记账,最后都变成手动填表」。我试过用表格记了三个月,结果微信、支付宝、银行卡三份账单格式完全不一样,月底对账时字段名对不上,分类全靠脑子记,最后不了了之。
后来我把思路换了一下:与其做一个「大而全的记账系统」,不如先做一个Agent Harness 骨架——也就是把模型调用通道、配置文件、任务路由这三件事先固定下来,让账单解析、分类记账、月度汇总这三类任务能跑通,剩下的功能再慢慢往里加。Harness 在这里不是某个具体框架的名字,而是指「把 Agent 的运行环境、配置、工具入口统一收口」的那层壳。
这篇要交付的东西很具体:一份config.toml、一份settings.json,加上一次端到端验证动作。你照着填,就能在私有环境里复现一个最小可用的记账 Agent。适合谁?有基础 Python 能力、想自己掌控财务数据、又不想从零造轮子的开发者。核心检索词就三个:Agent、Harness、个人财务记账。
我踩过的坑是:一开始把 API Key 硬编码在脚本里,换模型时改了七八个文件。后来统一走一个 Key/API 通道,配置文件只留一份,问题就消失了。下面按这个思路展开。
2. TaoToken 前置:统一 Key 与 API 通道
2.1 为什么记账 Agent 需要一个统一通道
记账 Agent 的模型调用有三个特点:调用频繁(每笔账单解析都要过一次模型)、任务类型固定(解析、分类、汇总)、对稳定性敏感(月底汇总跑一半失败很烦)。如果每个任务各接一个模型供应商,Key 管理、限流、计费都会变成负担。
统一通道的价值在于:你只需要维护一个 API Key,模型切换、额度查看、调用日志都在一个地方。对私有部署来说,这意味着配置文件里只出现一个base_url和一个api_key,迁移环境时改两行就行。
2.2 拿到 Key 并确认通道可用
进入控制台创建 API Key,建议按用途命名,比如bill-agent-dev,方便后续区分开发和生产。创建后你会得到一串以sk-开头的密钥,只显示一次,先复制到安全的地方。
注意:不要把 Key 提交到 Git。配置文件里用占位符,真实值放环境变量或本地
.env。
通道地址统一用https://taotoken.net/api,不要带任何多余路径。模型名按你实际开通的填,比如gpt-4o-mini这类通用对话模型就够记账场景用。
2.3 三类任务与模型的对应关系
| 任务类型 | 输入 | 输出 | 建议模型能力 |
|---|---|---|---|
| 账单解析 | 原始 CSV/文本行 | 结构化 JSON | 指令遵循强、JSON 稳定 |
| 分类记账 | 交易备注+对手方 | 分类名+置信度 | 语义理解好、成本低 |
| 月度汇总 | 结构化账单列表 | 汇总文本+统计 | 长上下文、数值准确 |
这三类任务共用同一个通道,但可以在settings.json里给每类任务单独指定模型名和温度,互不干扰。
3. 可复制配置:config.toml 与 settings.json 骨架
3.1 目录结构先定下来
bill-agent/ ├── config.toml ├── settings.json ├── .env ├── agent/ │ ├── __init__.py │ ├── llm_client.py │ ├── tasks.py │ └── parser.py └── data/ └── bills.dbconfig.toml管通道和运行参数,settings.json管任务级配置。分开的原因是:前者换环境才动,后者调优时经常改。
3.2 config.toml 完整骨架
# config.toml [llm] base_url = "https://taotoken.net/api" api_key = "${TAOTOKEN_API_KEY}" timeout = 60 max_retries = 3 [agent] name = "bill-harness" data_dir = "./data" db_path = "./data/bills.db" log_level = "INFO" [harness] # 任务路由:把任务名映射到 settings.json 里的配置块 task_map = { bill_parse = "parse", bill_classify = "classify", monthly_summary = "summary" }api_key用${TAOTOKEN_API_KEY}占位,运行时从环境变量读取。这样配置文件可以放心提交,Key 留在本地。
3.3 settings.json 完整骨架
{ "parse": { "model": "gpt-4o-mini", "temperature": 0.0, "system_prompt": "你是账单解析器。把输入的一行交易记录解析为 JSON,字段:time, amount, direction, counterpart, remark。只输出 JSON,不要解释。", "max_tokens": 512 }, "classify": { "model": "gpt-4o-mini", "temperature": 0.1, "system_prompt": "你是记账分类器。根据 remark 和 counterpart 判断分类,从给定分类列表中选择。输出 JSON:{category, confidence}。", "categories": ["餐饮", "交通", "住房", "购物", "娱乐", "医疗", "宠物", "其他"], "max_tokens": 128 }, "summary": { "model": "gpt-4o-mini", "temperature": 0.3, "system_prompt": "你是财务汇总助手。根据传入的账单列表,输出本月总支出、分类占比、环比变化。用简洁中文。", "max_tokens": 1024 } }三个配置块对应三类任务,task_map负责把任务名路由过去。加新任务时,只需在settings.json加一块、在task_map加一行映射。
3.4 读取配置的客户端代码
# agent/llm_client.py import os import json import tomllib from openai import OpenAI def load_config(path="config.toml"): with open(path, "rb") as f: cfg = tomllib.load(f) cfg["llm"]["api_key"] = os.environ.get("TAOTOKEN_API_KEY", cfg["llm"]["api_key"]) return cfg def load_settings(path="settings.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) class LLMClient: def __init__(self, config, settings): self.config = config self.settings = settings self.client = OpenAI( base_url=config["llm"]["base_url"], api_key=config["llm"]["api_key"], timeout=config["llm"]["timeout"], ) def run_task(self, task_name, user_input): task_key = self.config["harness"]["task_map"][task_name] task_cfg = self.settings[task_key] resp = self.client.chat.completions.create( model=task_cfg["model"], temperature=task_cfg["temperature"], max_tokens=task_cfg["max_tokens"], messages=[ {"role": "system", "content": task_cfg["system_prompt"]}, {"role": "user", "content": user_input}, ], ) return resp.choices[0].message.content这段代码是 Harness 的核心:任务名进,配置自动匹配,模型调用统一出口。你不需要在每个任务里重复写base_url和api_key。
4. 验证请求:一次端到端跑通
4.1 准备环境变量
export TAOTOKEN_API_KEY="sk-你的真实Key"Windows 用set或写进.env配合python-dotenv。确认环境变量生效:
python -c "import os; print(os.environ.get('TAOTOKEN_API_KEY')[:6])"输出前 6 位说明读取成功。
4.2 写一个最小验证脚本
# verify.py from agent.llm_client import load_config, load_settings, LLMClient config = load_config() settings = load_settings() client = LLMClient(config, settings) # 任务一:账单解析 raw = "2024-06-15 12:30:00 | -28.50 | 支出 | 瑞幸咖啡 | 生椰拿铁" parsed = client.run_task("bill_parse", raw) print("解析结果:", parsed) # 任务二:分类记账 classified = client.run_task("bill_classify", parsed) print("分类结果:", classified) # 任务三:月度汇总 summary = client.run_task("monthly_summary", f"账单列表:{parsed}") print("汇总结果:", summary)4.3 预期成功结果
解析任务应返回类似:
{"time": "2024-06-15 12:30:00", "amount": 28.50, "direction": "支出", "counterpart": "瑞幸咖啡", "remark": "生椰拿铁"}分类任务应返回:
{"category": "餐饮", "confidence": 0.95}汇总任务会输出一段中文,包含总支出和分类占比。三个任务都返回结构化结果,说明通道、配置、路由全部打通。
4.4 把结果落库
# agent/parser.py import sqlite3 import json def save_bill(db_path, parsed_json): data = json.loads(parsed_json) conn = sqlite3.connect(db_path) conn.execute( "INSERT INTO bills (time, amount, direction, counterpart, remark) VALUES (?, ?, ?, ?, ?)", (data["time"], data["amount"], data["direction"], data["counterpart"], data["remark"]), ) conn.commit() conn.close()建表语句:
CREATE TABLE IF NOT EXISTS bills ( id INTEGER PRIMARY KEY AUTOINCREMENT, time TEXT NOT NULL, amount REAL NOT NULL, direction TEXT NOT NULL, counterpart TEXT, remark TEXT, category TEXT );到这里,Harness 骨架就跑通了:配置驱动、任务路由、模型调用、结果落库,四步闭环。
5. 本篇常见错排查
5.1 401 或鉴权失败
最常见原因是环境变量没生效,或者 Key 复制时带了空格。先确认:
python -c "import os; print(repr(os.environ.get('TAOTOKEN_API_KEY')))"如果输出None,说明变量没设上。如果输出带引号或空格,说明复制时多了字符。另外检查config.toml里base_url是否写成了带路径的形式,统一通道只需要https://taotoken.net/api。
5.2 模型返回不是 JSON
解析任务对 JSON 稳定性要求高。如果模型返回了多余解释,把temperature调到 0,并在 system prompt 里加一句「只输出 JSON,不要 markdown 代码块」。如果还是不稳定,可以在代码里做一次清洗:
import re def extract_json(text): match = re.search(r"\{.*\}", text, re.DOTALL) return match.group(0) if match else text5.3 分类结果总是「其他」
检查settings.json里categories列表是否和你的实际消费场景匹配。如果分类太少,模型只能往「其他」塞。建议先列 8 到 12 个高频分类,每个分类在 prompt 里给一两个关键词示例,比如「餐饮:瑞幸、星巴克、外卖」。
5.4 月度汇总数值对不上
汇总任务的输入是结构化账单列表,如果前面解析阶段金额符号搞错(支出记成正数),汇总就会偏。建议在解析 prompt 里明确「支出为负数,收入为正数」,并在落库前做一次校验:
assert data["direction"] in ("支出", "收入") if data["direction"] == "支出": data["amount"] = -abs(data["amount"])5.5 超时或重试风暴
timeout设 60 秒够用,max_retries设 3 次。如果频繁超时,先检查网络,再检查是不是单次输入太长。账单解析建议一行一行传,不要一次塞几百行。
5.6 配置文件改了不生效
tomllib是启动时读取的,改完config.toml要重启进程。settings.json如果在代码里做了缓存,也要注意刷新。调试阶段可以在每次run_task前重新加载,生产环境再改成启动加载。
6. 下一步:把骨架接进你的工作流
骨架跑通后,接下来三件事按优先级排:
第一,把账单解析接到真实数据源。微信和支付宝的 CSV 编码不同,先做编码探测,再按行喂给解析任务。第二,给分类任务加一个本地缓存,同一备注第二次出现直接查缓存,省调用也省时间。第三,月度汇总做成定时任务,每月 1 号自动跑,结果推到你的通知渠道。
如果你要长期跑编码和 Agent 任务,可以了解 Coding Plan,把开发环境和运行环境分开管理。需要验证模型对话效果,直接进模型对话页面试。接入过程中遇到鉴权或路由问题,先看 API Keys 管理页确认 Key 状态,再对照接入文档检查base_url和模型名。
配置文件骨架的价值不在于一次写完美,而在于它把「模型调用」和「业务逻辑」隔开了。你换模型、换供应商、加任务,都只动配置,不动代码。这才是 Harness 工程化落地最实在的部分。