周末花了两天做了一组实验假设自己是一个没系统学过 Agent 开发的普通后端只看文档用 DeepSeek 的 API 和它外层那套工具调用体系到底能折腾出什么。实验结果比我预想得顺利不少——Day 1 搭通了 OpenAI 兼容接口和 Function Calling 的最小闭环Day 2 给助手加上 SQLite 持久化做了一个真正能用的命令行待办管理工具。整个过程最强烈的感受是DeepSeek Harness 太权威了。这里的“权威”不是指模型回答有多厉害而是指模型外层那套工具层、接口层、调用规范已经成熟到普通人几乎不用自己发明轮子。tools 怎么写、消息怎么回传、工具结果怎么接回对话官方文档和社区惯例都铺好了路你只需要把业务逻辑填进去。这篇文章不是严格意义上的教程更像一份带完整代码和踩坑记录的实验笔记。即便如此关键代码、依赖和排查思路我都会留下读者按顺序操作半小时左右就能复现 Day 1 的效果。如果你是第一次接触大模型应用开发看完之后至少能搞清楚三件事DeepSeek API 到底怎么接入、Function Calling 的消息闭环长什么样、一个“模型 工具 本地存储”的最小 Agent 怎么搭起来。如果你已经写过一些 OpenAI 兼容接口的代码可以重点看 Day 2 的工程化处理和第 7 节的排错清单。1. 先说结论普通人两天能做到什么程度在开始之前先给这次实验划一个明确的边界。所谓“普通人”我的设定是没有深入研究过强化学习、不微调模型、不读源码也不打算花时间研究 Agent 框架只会 Python、会查官方文档、有基本后端经验。实验过程中完全不碰模型训练和提示词工程的深水区只依赖现成的 API 能力和标准工具调用协议。两天时间我做成了这些东西。Day 1 完成最小工具闭环模型能根据用户问题判断是否该调用工具能输出结构化调用参数本地代码执行工具后把结果回传给模型模型再基于真实结果生成回答。Day 2 做了一个待办管理助手支持新增、查询、完成待办数据落在 SQLite 里重启程序数据不丢。严格说这个助手的代码量很少核心文件加起来不到 200 行但它已经是一个完整的“能对话、能行动、有记忆”的小 Agent。如果要对这两天的成果打个分我的评价是单看功能很朴素但“从零到能用”的过程非常快。我几乎没有遇到需要绕路解决的难题大部分时间花在熟悉 API 参数和调试消息格式上而不是在研究模型为什么调用错工具。换句话说DeepSeek 背后的 Harness 体系把最难的工程问题消化掉了普通人只需要站在上面写业务。当然也有明显的边界。两天时间做出来的东西经不起并发、没有鉴权、没有日志体系工具执行层也还停留在“能跑”的阶段。这些不是模型的问题而是工程化投入的问题。所以第 8 节我会单独讲“两天之外”还要补哪些课避免你把这些实验代码直接丢到生产环境。2. 为什么说“Harness 太权威了”2.1 我理解的 Harness 是什么Harness 这个词在 LLM 应用开发里通常指模型外面那层“约束与驱动系统”。模型本身只会根据上下文预测下一个 token它不会主动查询数据库、不会读取本地时间、不会操作文件。要让模型真正帮用户办事就必须在外层搭一套机制定义工具、解析模型的调用意图、执行工具、把结果反馈给模型。这套机制就是 Harness。DeepSeek Harness 可以理解为“模型能力 官方 API OpenAI 兼容层 Function Calling 工具协议”的组合。你不需要自己设计“模型怎么表达调用意图”这件事因为 API 已经定义了标准答案模型在回答中输出tool_calls字段里面包含函数名和参数。你只需要照着这个字段解析、执行、回传剩下的交给模型。在更广义的语境里LangChain、LlamaIndex 这类框架也属于 Harness 的一种实现它们把工具注册、记忆管理、任务规划都封装成了组件。不过这这次实验我没有用任何 Agent 框架而是直接基于 DeepSeek API 手工搭建最简闭环这样能更清楚地看到每一层到底发生了什么。2.2 “权威”具体体现在哪我之前对“普通人自己搭工具调用”这件事是有心理预期的总觉得要处理很多边缘情况比如模型不按格式输出、工具调用参数幻觉、多轮调用状态混乱。实际跑下来这些坑大部分已经被 API 设计挡掉了。第一是兼容性权威。DeepSeek API 对 OpenAI 协议做了高度兼容OpenAI SDK 只需要修改base_url和api_key就能直接调用代码迁移成本几乎为零。这意味着社区里大量基于 OpenAI SDK 的工具、示例、中间件都可以直接平移到 DeepSeek 上。第二是工具协议权威。tools 参数采用 JSON Schema 描述函数签名name、description、parameters、required这些字段定义足够表达绝大多数场景。模型对这套协议的理解很稳定我在实验中很少遇到“该调用工具却不调用”或者“乱传参数”的情况说明模型层面的指令遵循能力是过关的。第三是生态权威。因为协议是开放的SQLite、日志、错误处理、重试机制这些通用工程组件都可以直接复用。两天的实验里我真正从零写的只有工具函数和调度循环其余全是现成套路。这种“权威”带来的直接结果是普通开发者的门槛被压得很低核心竞争力重新回到业务理解上。3. 环境准备与成本控制3.1 前置条件实验环境非常简单只要满足下面几项就可以开始Python 3.9 及以上版本本次实验没有用到 3.10 之后的新语法兼容性比较宽松。OpenAI Python SDK安装命令是pip install openai。如果已经有了旧版本建议升级到较新版本避免参数格式差异。一个 DeepSeek 开放平台账号并创建 API Key。SQLite 直接用 Python 标准库sqlite3不需要额外安装。能正常访问api.deepseek.com的网络环境即可。创建 API Key 的入口在 DeepSeek 开放平台的控制台创建后把 Key 复制下来。注意 Key 只会完整显示一次丢失后需要重新生成。实际开发中不要把这个 Key 直接写进代码仓库建议放到环境变量里读取例如export DEEPSEEK_API_KEYsk-你的Key然后通过os.getenv(DEEPSEEK_API_KEY)获取。示例代码里为了方便展示会直接写字符串但你自己跑的时候一定要改成环境变量方式防止把密钥提交到 Git。项目结构也不需要复杂两天实验中我使用的是下面这种布局deepseek-experiment/ ├── quick_test.py # 最小接入测试 ├── tools_def.py # 工具定义 ├── model_with_tools.py # Day 1 工具调度循环 ├── todo_store.py # SQLite 存储层 └── day2_todo_agent.py # Day 2 待办助手3.2 最小接入代码先跑通最基础的非工具调用确保 API Key 和环境没有问题。保存quick_test.py后直接运行# quick_test.py import os from openai import OpenAI client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY, sk-你的Key), base_urlhttps://api.deepseek.com, ) resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: 你是一个简洁、直接的中文助手。}, {role: user, content: 用一句话介绍 HTTP 与 HTTPS 的区别。}, ], max_tokens256, temperature0.7, ) print(resp.choices[0].message.content)几个参数先解释一下。model指定模型名实验中使用的是deepseek-chat这是 DeepSeek 对外提供的主力对话模型支持工具调用适合绝大多数日常任务如果需要更强推理能力可以尝试deepseek-reasoner但工具调用的具体表现要以官方文档为准。messages是完整的对话历史system消息用来设定角色约束user消息是用户输入。max_tokens限制生成的最大 token 数防止答案过长也方便控制成本。temperature控制随机性日常助手场景 0.7 左右比较合适需要稳定输出的场景建议调低。运行后如果能看到一段关于 HTTP 与 HTTPS 区别的中文回答说明接入已经打通。如果报错优先检查api_key是否正确、环境变量有没有被读取到。3.3 成本与限流观察DeepSeek API 按 token 计费具体单价会随版本和活动变化这里不做数字断言以官方价格页面为准。实验代码量小两天跑下来消耗非常有限但对于真实项目有几个成本控制习惯建议尽早养成。每次响应对象里通常带usage字段包含prompt_tokens和completion_tokens可以在代码里打印出来做成本估算usage resp.usage print(f输入 tokens: {usage.prompt_tokens}, 输出 tokens: {usage.completion_tokens})另外要注意的是消息历史会越积越长。每次调用都把完整历史发给模型输入 token 就会不断膨胀。对于实验脚本这不是问题但对于长时间运行的服务必须做历史裁剪或摘要压缩否则钱会悄无声息地烧掉。限流方面高频调用时可能遇到 429 错误。处理思路是加退避重试例如失败后等 1 秒、2 秒、4 秒递增重试。不要无限重试控制在 3 到 5 次即可。对实验来说正常调用频率根本不会触发限流所以先把退避机制写在工具函数里备用就好。4. 核心原理大模型的工具调用4.1 一句话讲清 Function Calling在深入代码之前有必要把工具调用的原理讲透。大模型本身没有“执行”能力它只会根据上下文预测下一个 token。所谓 Function Calling其实是模型在回答中输出一个结构化的“调用意图”告诉你它想调用哪个工具、传什么参数。真正执行工具的始终是本地代码。一次完整的工具调用闭环可以用下面的 ASCII 流程表示用户输入 ↓ 调用 Chat Completion API携带 tools 工具描述 ↓ 模型返回 assistant 消息 ├─ 不需要工具直接回复文本 └─ 需要工具返回 tool_calls [{name, arguments}] ↓ 本地 execute_tool() 真正执行 ↓ 把结果作为 roletool 消息追加进 messages ↓ 再次请求模型生成基于工具结果的回答这个循环中模型只负责“判断和表达”本地代码负责“执行和反馈”。理解这一点后你会发现 Agent 并没有那么神秘核心就是一个 while 循环加一个工具路由表。4.2 tools 参数怎么写tools 参数本质上是一个 JSON Schema 风格的工具描述列表。每个工具需要说明三件事工具叫什么、工具能做什么、参数长什么样。# tools_def.py TOOLS [ { type: function, function: { name: get_current_time, description: 获取当前本地日期和时间返回 ISO 格式字符串, parameters: { type: object, properties: {}, required: [] } } }, { type: function, function: { name: calc, description: 执行简单的四则运算适合模型不擅长的精确计算, parameters: { type: object, properties: { expr: { type: string, description: 需要计算的表达式例如 123*456789 } }, required: [expr] } } } ]关键点有三个。name要短而语义化模型会依赖这个名字选择工具description要写得足够清楚模型判断“该不该调用这个工具”时主要看 descriptionparameters用 JSON Schema 描述参数结构务必把每个字段的description写明白模型填充参数时才有依据。required列表声明哪些参数必须提供不声明的参数模型可以选择不传。4.3 消息序列与调用闭环工具调用最容易出错的地方不在请求而在消息序列的构造。模型返回的 assistant 消息如果带tool_calls这条消息必须以原样追加到 messages 里然后再逐条追加每个工具的执行结果结果消息的role必须是tool并且tool_call_id要和 assistant 消息里的调用 id 一一对应。下面的消息序列是一个标准范例messages [ {role: system, content: ...}, {role: user, content: 现在几点了}, {role: assistant, content: None, tool_calls: [ {id: call_1, function: {name: get_current_time, arguments: {}}} ]}, {role: tool, tool_call_id: call_1, content: {\now\: \2025-08-20T09:41:27\}}, ]很多人的 Agent 卡在“模型反复调用同一个工具但拿不到结果”原因就是没有把 assistant 消息原样回传或者tool_call_id对不上。带着这条经验Day 1 的代码就很好理解了。5. Day 1 实验打通最小工具闭环5.1 目标与工具清单Day 1 的目标非常简单让模型在一个回合内走完“判断 → 调用 → 执行 → 反馈 → 回复”的完整链路。我准备了两工具一个是get_current_time测试模型能否在需要时间信息时主动调用工具另一个是calc测试模型能否在遇到精确计算时把计算任务交给工具而不是自己硬算。选择这两个工具是有讲究的。当前时间是一个模型无法从训练数据里“记住”的信息必须依赖工具精确计算则是模型经常出错、但工具可以零误差完成的场景。这两个点能清楚验证工具调用是否真的生效。5.2 工具执行与调度代码核心代码分成两部分工具执行函数和调度循环。工具执行函数是“真正干活的地方”调度循环负责和模型对话。# model_with_tools.py import json import datetime from openai import OpenAI from tools_def import TOOLS client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY, sk-你的Key), base_urlhttps://api.deepseek.com, ) def execute_tool(name: str, args: dict): 真正干活的地方模型只负责说出想调用谁执行权在本地。 if name get_current_time: return {now: datetime.datetime.now().isoformat(timespecseconds)} if name calc: expr args.get(expr, ).strip() if not expr or any(ch not in 0123456789-*/(). for ch in expr): return {error: expression contains illegal characters} try: # 仅演示用途生产环境请使用 ast.parse 或更安全的计算方案 return {result: eval(expr)} except Exception as e: return {error: str(e)} return {error: funknown tool: {name}}execute_tool的职责很纯粹根据工具名路由到对应函数返回一个 JSON 序列化友好的字典。注意calc里我加了一层字符白名单校验只允许数字、四则运算符号和括号进入eval这是最低限度的安全防护。eval在真实项目中仍然不推荐稍后第 8 节会专门讨论替代方案。调度循环是整篇文章最重要的一段代码def ask(user_input: str, max_rounds: int 5): messages [ {role: system, content: 你是本地助手。需要时间或精确计算时调用工具后再回答。}, {role: user, content: user_input}, ] for _ in range(max_rounds): resp client.chat.completions.create( modeldeepseek-chat, messagesmessages, toolsTOOLS, max_tokens512, ) msg resp.choices[0].message # 模型没有工具调用意图直接输出最终回答 if not msg.tool_calls: print(助手:, msg.content) return # 模型想调用工具先打印日志再把 assistant 消息加入历史 print(模型请求调用工具:, [(c.function.name, c.function.arguments) for c in msg.tool_calls]) messages.append(msg) # 逐个执行工具并把结果以 roletool 追加 for call in msg.tool_calls: args json.loads(call.function.arguments) result execute_tool(call.function.name, args) print(f[执行工具] {call.function.name}({args}) - {result}) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse), }) print(达到最大工具调用轮次停止。)max_rounds参数是防死循环的保险丝。如果模型和工具之间出了问题比如工具结果一直没被正确消费循环会一直进行下去每次都在烧 token。限制轮次后程序会在达到上限时主动退出并提示。5.3 典型运行结果运行下面两个测试用例if __name__ __main__: ask(现在几点了) ask(帮我算一下 123*456789 等于多少然后告诉我结果。)一次典型输出如下节选时间会根据实际运行时刻变化模型请求调用工具: [(get_current_time, {})] [执行工具] get_current_time({}) - {now: 2025-08-20T09:41:27} 助手: 现在时间是 2025-08-20 09:41:27。 模型请求调用工具: [(calc, {expr: 123*456789})] [执行工具] calc({expr: 123*456789}) - {result: 56877} 助手: 123*456789 的结果是 56877。注意第二个例子模型没有自己硬算而是选择把表达式原样交给calc工具说明 tools 描述里的description起到了预期的引导作用。Day 1 到这里就算达标了最小工具闭环已经跑通。6. Day 2 实验做成能用的待办助手6.1 需求与设计Day 1 的代码能演示原理但离“能用”还有距离。Day 2 的目标是把它升级成一个有实际价值的工具待办管理助手。需求拆解下来只有四件事新增待办、查看待办列表、把待办标记为完成、查询当前时间。其中前三个都需要数据持久化所以引入 SQLite。设计上分成两层存储层只负责数据读写不关心模型Agent 层负责把自然语言翻译成对存储层的调用。存储层是普通 Python 代码可以单独测试Agent 层只通过工具函数和存储层交互两者耦合度很低。这样拆的好处是以后想换数据库或者换模型改动范围都很小。6.2 SQLite 存储层SQLite 是 Python 标准库自带的数据库非常适合这种单机小工具。存储层代码很简单但已经把建表、增、查、改的核心方法都覆盖了。# todo_store.py import sqlite3 class TodoStore: def __init__(self, db_pathtodo.db): self.conn sqlite3.connect(db_path) self.conn.execute( CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, priority TEXT NOT NULL DEFAULT 中, done INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)) ) ) self.conn.commit() def add(self, content: str, priority: str 中) - int: cur self.conn.execute( INSERT INTO todos(content, priority) VALUES (?, ?), (content, priority), ) self.conn.commit() return cur.lastrowid def list_todos(self, doneNone): sql SELECT id, content, priority, done FROM todos params () if done is not None: sql WHERE done ? params (1 if done else 0,) sql ORDER BY done ASC, id DESC rows self.conn.execute(sql, params).fetchall() return [ {id: r[0], content: r[1], priority: r[2], done: bool(r[3])} for r in rows ] def finish(self, todo_id: int) - bool: cur self.conn.execute(UPDATE todos SET done 1 WHERE id ?, (todo_id,)) self.conn.commit() return cur.rowcount 0这里要注意两点。第一所有 SQL 都使用参数化查询绝不要把模型输出的内容直接拼进 SQL 字符串这是最基本的注入防护。第二finish方法返回cur.rowcount 0用来判断传入的 id 是否存在Agent 层可以把“该待办不存在”的信息反馈给模型避免模型给出错误回答。6.3 多工具 Agent 完整代码存储层准备好之后Agent 层的代码和 Day 1 结构一致只是工具数量从两个增加到四个。# day2_todo_agent.py import json import datetime import os from openai import OpenAI from todo_store import TodoStore client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY, sk-你的Key), base_urlhttps://api.deepseek.com, ) store TodoStore() TODO_TOOLS [ { type: function, function: { name: add_todo, description: 新增一条待办事项, parameters: { type: object, properties: { content: {type: string, description: 待办内容例如周三下午开会}, priority: {type: string, enum: [高, 中, 低], description: 优先级默认中} }, required: [content] } } }, { type: function, function: { name: list_todos, description: 查询待办列表可筛选未完成或已完成, parameters: { type: object, properties: { done: {type: string, enum: [pending, finished], description: 筛选条件} }, required: [] } } }, { type: function, function: { name: finish_todo, description: 把指定 id 的待办标记为已完成, parameters: { type: object, properties: { todo_id: {type: integer, description: 待办 id} }, required: [todo_id] } } }, { type: function, function: { name: get_current_time, description: 获取当前日期和时间, parameters: {type: object, properties: {}} } } ] def execute_tool(name: str, args: dict): if name add_todo: return {id: store.add(args[content], args.get(priority, 中))} if name list_todos: done_filter args.get(done, all) done {pending: False, finished: True}.get(done_filter) return {todos: store.list_todos(done)} if name finish_todo: return {ok: store.finish(args[todo_id])} if name get_current_time: return {now: datetime.datetime.now().isoformat(timespecseconds)} return {error: funknown tool: {name}} def ask(user_input: str, max_rounds: int 5): messages [ {role: system, content: 你是待办助手。新增、查询、完成待办时必须调用工具不要凭空编造数据。回答用简洁中文。}, {role: user, content: user_input}, ] for _ in range(max_rounds): resp client.chat.completions.create( modeldeepseek-chat, messagesmessages, toolsTODO_TOOLS, max_tokens512, ) msg resp.choices[0].message if not msg.tool_calls: print(助手:, msg.content) return messages.append(msg) for call in msg.tool_calls: args json.loads(call.function.arguments) result execute_tool(call.function.name, args) print(f[执行工具] {call.function.name}({args}) - {result}) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse), }) print(达到最大工具调用轮次停止。) if __name__ __main__: ask(帮我记三件事买牛奶高优先级给导师发邮件周五提交周报。优先级默认就行。) ask(现在有哪些没做完的) ask(把买牛奶那条标成已完成再告诉我现在的待办列表。)和 Day 1 相比主要的工程化改动在 system prompt 和工具返回语义。system prompt 里明确写了“必须调用工具不要凭空编造数据”这句话非常关键。没有它模型可能会在看到“查待办”时自己编一份看起来合理的列表而不是去查询真实数据库。工具函数也都返回结构化字段比如{ok: true}或{id: 3}模型能顺着这些结果组织自然语言回答。6.4 运行演示运行day2_todo_agent.py后一次典型输出如下用户: 帮我记三件事买牛奶高优先级给导师发邮件周五提交周报。优先级默认就行。 [执行工具] add_todo({content: 买牛奶, priority: 高}) - {id: 1} [执行工具] add_todo({content: 给导师发邮件}) - {id: 2} [执行工具] add_todo({content: 周五提交周报}) - {id: 3} 助手: 已经记好了 1. 买牛奶高优先级 2. 给导师发邮件中优先级 3. 周五提交周报中优先级 用户: 现在有哪些没做完的 [执行工具] list_todos({done: pending}) - {todos: [{id: 1, ...}, {id: 2, ...}, {id: 3, ...}]} 助手: 还有 3 条未完成 1. 买牛奶高优先级 2. 给导师发邮件中优先级 3. 周五提交周报中优先级 用户: 把买牛奶那条标成已完成再告诉我现在的待办列表。 [执行工具] finish_todo({todo_id: 1}) - {ok: true} [执行工具] list_todos({done: all}) - {todos: [{id: 2, ...}, {id: 3, ...}]} 助手: 已经帮你把“买牛奶”标记为完成了。当前待办还剩 2 条 1. 给导师发邮件中优先级 2. 周五提交周报中优先级注意第一条输入里包含三个待办模型在一个响应中并行发起了三次add_todo调用说明工具调用协议支持一次返回多个tool_calls调度循环里for call in msg.tool_calls正好覆盖了这种情况。程序重启后再执行ask(现在有哪些没做完的)数据依然在因为 SQLite 已经把结果持久化到todo.db文件里了。到这里Day 2 的实验目标完成一个能对话、能操作真实数据、重启不丢数据的待办助手。7. 高频问题与排查思路两天的实验里我踩了一些坑也整理了群里朋友常问的问题统一放在下面的表格里。如果你遇到类似报错可以按表格里的思路排查。问题现象常见原因解决思路400 错误提示Invalid parametermodel 名称写错或 tools 格式不符合 JSON Schema确认模型名是deepseek-chat检查 tools 的name、parameters字段是否合法401 Authentication FailsAPI Key 错误、未生效或环境变量没读取到检查api_key是否传对重新生成 Key 后等待几分钟再试402 Insufficient Balance账户余额不足去开放平台充值或先降低max_tokens减少消耗429 Rate limit reached请求频率超过限制加入退避重试控制并发避免死循环导致连续请求模型反复调用同一个工具工具结果没有以roletool正确回传检查 assistant 消息是否原样追加tool_call_id是否对应json.loads解析 arguments 失败模型偶发输出非严格 JSON打印原始字符串定位问题用 try/except 兜底必要时让模型修正SQLite 数据不持久化每次运行创建新的数据库对象但路径不一致固定db_path例如todo.db不要使用临时路径工具返回了数据但模型不引用system prompt 没有明确约束在 system prompt 中写明“必须基于工具结果回答不要编造”这里单独解释一下“模型反复调用同一个工具”这个现象。它的根因通常是消息序列断链模型发出了tool_calls但你只把工具执行结果塞回历史却没有把带tool_calls的 assistant 消息一起塞回去模型就不知道自己刚才发过调用请求于是再问一遍。正确做法是先messages.append(msg)再逐条追加tool消息。另外tools参数不要随便传空列表或者传 None这样等价于告诉模型“没有工具可用”模型遇到需要外部信息的问题时就会硬答。如果只是闲聊不需要工具可以不传tools字段而不是传空列表。8. 工程建议两天之外的边界实验代码能跑通原理但距离生产使用还有一段距离。这一节梳理几个重要的工程化方向也是我在 Day 2 结束之后最想补充的部分。8.1 安全边界Agent 工具调用天然存在一个风险模型输出的参数不可预测工具函数必须对外部输入保持警惕。第一层防护是工具参数校验比如finish_todo的todo_id必须限定为整数不能接受模型传进来的任意字符串。第二层防护是执行白名单如果工具涉及文件操作、命令执行、网络请求一定要把可操作范围限制死。第三层防护是清理模型输出不要把模型生成的内容直接拼进 SQL、Shell 命令或 HTML。Day 1 代码里的eval就是反例。它只是实验演示生产环境应该改用ast.parse做语法校验后再求值或者直接调用成熟的表达式解析库。数据库操作必须使用参数化查询防止提示词注入通过工具参数进入 SQL。日志里也不要记录完整的 API Key 和用户敏感数据脱敏之后再落盘。8.2 可观测性与成本控制Agent 的调试比普通接口难因为一次用户请求可能包含多次模型调用和工具调用。建议至少记录三样东西完整的消息序列、每次调用的usage字段、工具函数的输入输出。我的实验代码里用print打印了工具调用日志真实项目里应该换成结构化日志把tool_call_id、函数名、参数、耗时、token 消耗都记录下来。成本控制上除了之前说的历史裁剪还可以给max_tokens设一个合理的上限。Agent 循环里的每次模型调用都独立计费max_rounds5意味着一次用户问题最多可能产生 5 次模型请求所以轮次上限也是成本上限不要设得过大。8.3 真要上线还要注意什么如果要把这类助手做成服务至少还要补齐以下能力第一加上用户鉴权和接口限流防止被刷接口第二处理超时和重试OpenAI SDK 默认有超时机制但 Agent 循环里的每次调用都要考虑网络抖动第三工具函数要尽量幂等比如“标记完成”重复执行不应该产生副作用第四数据库要加备份策略SQLite 文件小直接定期复制文件即可第五多轮会话需要引入会话 ID把不同用户的消息历史隔离。另外模型输出永远是概率性的Agent 链路越长出错的概率就越大。上线前一定要设计兜底话术比如工具调用轮次达到上限、工具执行异常、模型输出无法解析时都给用户一个明确的提示而不是静默失败。9. 总结与下一步两天的实验让我对“普通人 权威 Harness”这件事有了更具体的认知。DeepSeek 的 API 和工具调用协议把 Agent 开发的门槛压得足够低低到一个人不需要研究模型细节只要会写 Python、会定义工具、会维护消息序列就能在两天内做出一个能落地的待办助手。这个过程里真正的难点不是模型调用而是工程细节消息怎么回传、历史怎么裁剪、数据怎么持久化、成本怎么控制。如果你也想做类似的两天实验我的建议是从最小闭环开始先跑通一个工具调用再加第二个工具最后才考虑持久化和复杂对话。不要一上来就上 Agent 框架手工搭一遍才能理解框架帮你解决了什么问题。下一步可以尝试的方向包括给助手加一个网络搜索工具、用deepseek-reasoner处理需要深度推理的任务、把历史消息做摘要压缩以支持超长对话、或者把命令行程序改造成 Web 服务。如果这篇实验记录对你有帮助可以先收藏备用。跑通之后如果想继续深入建议重点研究提示词工程、RAG 检索增强生成和 Agent 评估这三个方向它们是“能用”走向“好用”的关键。欢迎在评论区交流你的两小时、两天或两周实验成果我很想看看大家会在同一个 Harness 上长出什么不一样的东西。