☰
Agent知识学习笔记——01 Prompt/Function call/记忆/上下文:用TaoToken统一Key跑通四要素最小闭环
2026/10/11 15:05:37 网站建设 项目流程

1. 从一次“模型说退款了但钱没动”说起

如果你刚开始接触 Agent 开发,大概率会遇到这样一个场景:你写好了提示词,接上了模型,用户说“我要退款”,模型回复“已为您发起退款申请,请耐心等待”。看起来一切正常,但你去查订单系统,发现根本没有退款记录。问题出在哪?模型只是“说”了这句话,它并没有真的调用你的退款函数。

这就是 Agent 开发里最容易被忽略的认知门槛:LLM API 是无状态的。每次/chat/completions请求都是独立的 HTTP 调用,模型不记得上一轮说了什么,也不知道你有哪些工具可用。它只对本次传入的messages数组做一次前向计算,然后返回结果。模型侧没有状态,所有状态都在你的工程代码里。

由此推导出 Agent 四大基础要素的最小闭环:想让它“有人设”,你得每次都把提示词发一遍(Prompt);想让它“知道有哪些工具”,你得每次都把工具定义发一遍(Function call);想让它“记得”,你得把历史消息重新发一遍(记忆);而这三样东西加上本轮用户输入和工具定义,共同构成了每次请求的上下文(Context)。

这篇笔记面向刚接触 Agent 开发、想先跑通再深入的学习者。我会用 TaoToken 统一 Key 接入,依次验证 Prompt 模板渲染、Function call 触发与回传、记忆读写、上下文拼接是否符合预期,并记录每步的观测点和常见报错对照。你不需要先理解所有理论,跟着步骤跑一遍,看到日志里上下文条数从 2 变成 4 再变成 6,很多概念自然就通了。

2. TaoToken 前置准备:统一 Key 与 API 通道

在开始写代码之前,先把接入层准备好。TaoToken 提供统一的 API 通道,你只需要一个 Key 就能调用多种模型,省去分别注册和管理多个平台账号的麻烦。对于学习 Agent 四要素来说,这样可以把精力集中在 Prompt、Function call、记忆和上下文本身,而不是被接入细节分散注意力。

首先到官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册账号,然后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建好 Key 之后先复制保存,后面配置里要用。API 的基础地址是 https://taotoken.net/api ,注意这个地址后面不加 UTM 参数,直接用于代码里的baseUrl配置。

如果你习惯用命令行工具做快速验证,可以先用 curl 测一下 Key 是否可用。把下面的YOUR_API_KEY替换成你刚创建的 Key:

curl https://taotoken.net/api/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -d '{ "model": "gpt-4o-mini", "messages": [ {"role": "user", "content": "用一句话说明什么是无状态API"} ] }'

如果返回里有choices[0].message.content,说明 Key 和通道都正常。这一步的观测点是:HTTP 状态码 200,返回 JSON 里choices数组非空。如果返回 401,说明 Key 不对或没带上Authorization头;如果返回 404,检查一下 URL 是不是写成了带 UTM 的版本,API 调用地址就是https://taotoken.net/api这个前缀。

接下来准备项目依赖。我用的是 Java + LangChain4j 的组合,因为它在记忆管理和工具调用上的抽象比较清晰,适合观察四要素的运作过程。在pom.xml里加入以下依赖:

<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai</artifactId> <version>0.35.0</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>0.35.0</version> </dependency>

同时确认maven-compiler-plugin里开启了参数名保留,这个后面 Function call 参数映射会用到:

<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <parameters>true</parameters> </configuration> </plugin>

如果你用的是 Python,思路完全一样,把openai库的base_url指向https://taotoken.net/api即可。后面的配置片段我会以 Java 为主,但每个环节的观测点和验证方法跟语言无关。

3. 可复制配置:Prompt、Function call、记忆、上下文四件套

这一节给出可以直接复制的配置片段,把四个要素串起来。先看整体结构:一个Assistant接口负责对话入口,一个AssistantConfig负责组装模型、提示词、记忆和工具,一个RefundTool负责真正的副作用执行,一个ChatContextLogger负责把每轮发给模型的完整上下文打出来。

先写提示词文件,放在src/main/resources/prompt/agent.md。提示词要分段写清楚角色、任务、边界和工具调用要求:

# Role 你是一个电商售后助手,负责处理用户的退款诉求。 # Task 第一步:确认商品和问题描述。 第二步:判断是否属于质量问题。 第三步:调用工具发起退款,并告知用户单号。 # Limit 只处理质量问题导致的退款。 非质量问题不得调用 createRefund。 回复控制在 3 句话以内。 # Tools createRefund:仅在用户确认商品存在质量问题时调用。 参数 itemName 传商品名,reason 传问题描述。

注意这里显式写了“调用工具发起退款”。如果只写“我将为您发起退款”,模型大概率只输出这句话术而不触发工具调用,因为它认为输出这句话就已经完成任务了。这是 Prompt 和 Function call 配合的第一个关键点。

接着写Assistant接口。注意这里不写@SystemMessage注解,提示词从文件加载:

public interface Assistant { TokenStream chat(@MemoryId String sessionId, @UserMessage String userMessage); }

@MemoryId标注的参数就是会话 ID,框架会用它来隔离不同会话的记忆。然后是核心配置类:

@Configuration public class AssistantConfig { @Bean public Assistant assistant( @Value("${taotoken.base-url}") String baseUrl, @Value("${taotoken.api-key}") String apiKey, @Value("${taotoken.model-name}") String modelName, @Value("${assistant.prompt-path:prompt/agent.md}") String promptPath, @Value("${assistant.memory-max-messages:20}") int maxMessages, @Value("${assistant.log-context:true}") boolean logContext) { String systemPrompt = loadPrompt(promptPath); StreamingChatModel model = OpenAiStreamingChatModel.builder() .baseUrl(baseUrl) .apiKey(apiKey) .modelName(modelName) .listeners(logContext ? List.of(new ChatContextLogger()) : List.of()) .build(); return AiServices.builder(Assistant.class) .streamingChatModel(model) .systemMessageProvider(memoryId -> systemPrompt) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(maxMessages)) .tools(new RefundTool()) .build(); } private String loadPrompt(String promptPath) { try (InputStream in = new ClassPathResource(promptPath).getInputStream()) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); } catch (IOException e) { throw new IllegalStateException("加载提示词文件失败:" + promptPath, e); } } }

对应的application.yml配置:

taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model-name: gpt-4o-mini assistant: prompt-path: prompt/agent.md memory-max-messages: 20 log-context: true

这里base-url指向 TaoToken 的 API 地址,api-key从环境变量读取,避免硬编码。model-name可以换成你需要的模型 ID。三件套 Base URL、Key、Model ID 都在这里配齐了。

工具类RefundTool负责真正的副作用:

public class RefundTool { private static final Logger log = LoggerFactory.getLogger(RefundTool.class); @Tool("为用户发起退款申请。仅在用户已确认商品存在严重质量问题时调用,调用后款项按原路径退回") public String createRefund( @P("商品名称或描述,用户没说清楚时填未知商品") String itemName, @P("质量问题的具体描述,例如袖口开线") String reason) { String refundNo = "RF" + System.currentTimeMillis() + ThreadLocalRandom.current().nextInt(100, 1000); log.info("[模拟退款接口] 发起退款成功, refundNo={}, item={}, reason={}", refundNo, itemName, reason); return "退款申请已提交,退款单号 " + refundNo + ",款项将于 1-7 个工作日内退回原支付账户"; } }

@Tool的文字会变成工具描述,方法名变成工具名,@P的文字变成参数描述。这些文字就是模型选工具和填参数的全部依据,所以要写清楚。

最后是上下文日志监听器,这是观察四要素最重要的工具:

public class ChatContextLogger implements ChatModelListener { private static final Logger log = LoggerFactory.getLogger(ChatContextLogger.class); @Override public void onRequest(ChatModelRequestContext context) { List<ChatMessage> messages = context.chatRequest().messages(); StringBuilder sb = new StringBuilder(); sb.append("\n┌── 发给模型的完整上下文(共 ") .append(messages.size()).append(" 条)"); for (ChatMessage message : messages) { sb.append("\n│ ").append(render(message)); } sb.append("\n└──────────────────────────────"); log.info(sb.toString()); } private String render(ChatMessage message) { if (message instanceof SystemMessage systemMessage) { String text = systemMessage.text(); return "[system] " + firstLine(text) + " …(提示词全文 " + text.length() + " 字)"; } if (message instanceof UserMessage userMessage) { return "[user] " + userMessage.singleText(); } if (message instanceof AiMessage aiMessage) { StringBuilder sb = new StringBuilder("[ai] "); if (aiMessage.text() != null && !aiMessage.text().isBlank()) { sb.append(aiMessage.text()); } if (aiMessage.hasToolExecutionRequests()) { for (ToolExecutionRequest request : aiMessage.toolExecutionRequests()) { sb.append("\n│ └ 调用工具 ").append(request.name()) .append(' ').append(request.arguments()); } } return sb.toString(); } if (message instanceof ToolExecutionResultMessage resultMessage) { return "[tool:" + resultMessage.toolName() + "] " + resultMessage.text(); } return "[" + message.type() + "] " + message; } private String firstLine(String text) { int idx = text.indexOf('\n'); return idx > 0 ? text.substring(0, idx) : text; } }

这套配置跑起来之后,每轮请求都会在日志里打印出完整的消息列表。你可以清楚看到 system 提示词、历史消息、本轮输入、工具调用意图和工具返回值分别长什么样。

4. 逐步验证:从 Prompt 渲染到上下文拼接

配置就绪后,按顺序验证四个要素。每一步都有明确的观测点,看到预期结果再进入下一步。

4.1 验证 Prompt 模板渲染

启动应用后,先发一条最简单的消息:

curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId": "test-001", "message": "我买的衣服袖口开线了"}'

观测日志里ChatContextLogger的输出。第一轮应该看到 2 条消息:

┌── 发给模型的完整上下文(共 2 条) │ [system] # Role …(提示词全文 320 字) │ [user] 我买的衣服袖口开线了 └──────────────────────────────

如果 system 消息缺失,检查systemMessageProvider是否配置正确,以及提示词文件路径是否在 classpath 下。如果提示词内容为空,loadPrompt会抛异常导致启动失败,这是故意的,避免带着空提示词跑。

4.2 验证 Function call 触发与回传

继续发第二轮,确认商品问题:

curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId": "test-001", "message": "对的,就是质量问题"}'

这一轮日志会变得丰富。你会看到同一轮对话里出现了两次模型请求。第一次请求后,模型返回了工具调用意图:

│ [ai] │ └ 调用工具 createRefund {"itemName": "衣服", "reason": "袖口开线"}

然后框架执行RefundTool,日志里出现[模拟退款接口] 发起退款成功。接着第二次请求把工具返回值追加进去:

│ [tool:createRefund] 退款申请已提交,退款单号 RF1234567890…

最终模型基于工具返回值生成话术。这里的关键观测点是:createRefund的日志只出现一次,说明工具没有被重复调用;工具返回值里的单号出现在最终回复里,说明回传链路通了。

如果模型只输出“已为您发起退款”但没有调用工具那行,说明提示词里没有显式命令调用工具。回到提示词文件,把“调用 createRefund 工具”写进去。

4.3 验证记忆读写

发第三轮消息,测试模型是否记得之前的商品名:

curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId": "test-001", "message": "退款什么时候到账"}'

观测日志里的消息条数。如果记忆正常工作,这一轮应该看到 6 条或更多消息,包含前两轮的 user 和 ai 消息。模型回复里应该能提到“衣服”或之前的退款单号,说明它从历史消息里读到了信息。

这里有个容易困惑的点:模型并不“记得”商品名,它只是每次请求都收到了包含商品名的历史消息。记忆的本质就是“把历史重新发一遍”。浏览器每次只发了一句话,但后端从ChatMemory里取出历史拼进了请求。

4.4 验证上下文拼接

把三轮对话的日志连起来看,消息条数应该是 2 → 4 → 6 递增。每一轮新增两条:上一轮的 user 消息和 ai 回复。如果某轮工具被调用,还会多出工具调用意图和工具返回值两条。

你可以用ChatContextLogger的输出来核对:system 提示词每轮都在,且内容一致;历史消息按时间顺序排列;本轮用户输入在最后;工具定义虽然日志里看不到,但它每轮都作为请求的一部分发送。

到这里,四要素的最小闭环就跑通了。Prompt 通过systemMessageProvider注入到messages[0];Function call 通过@Tool定义、模型决策、框架执行、结果回传完成一轮交互;记忆通过@MemoryId和MessageWindowChatMemory实现按会话隔离的历史管理;上下文则是这三者加上本轮输入和工具定义的总和。

5. 常见报错排查对照

跑通的过程中难免遇到报错。这一节列出几个高频问题和排查方法,对照真实报错定位。

401 Unauthorized:最常见的原因是 API Key 没配或配错。检查application.yml里的api-key是否读到了环境变量,或者 curl 命令里的Authorization头是否带了Bearer前缀。如果 Key 是从控制台复制的,注意不要带多余空格。TaoToken 的 Key 在控制台 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 管理,可以重新生成一个再试。

local proxy failed / connection refused:这类报错通常出现在本地网络配置有问题时。检查base-url是否写成了https://taotoken.net/api,不要多加路径或参数。如果你在公司网络环境下,确认没有额外的网络策略拦截。这个报错跟 Key 无关,是连接层的问题。

reading choices 时返回空数组:说明请求发出去了,但模型没有返回有效内容。检查model-name是否是 TaoToken 支持的模型 ID。如果模型名写错,有些通道会返回空choices而不是明确报错。另外确认messages数组非空,且第一条消息的role是system或user。

OAuth 相关报错:如果你用的是某些需要 OAuth 认证的工具链,可能会看到 token 过期或 scope 不足的提示。TaoToken 的 API Key 方式是 Bearer Token,不涉及 OAuth 流程。如果工具链强制走 OAuth,检查它的配置是否指向了正确的认证端点。

工具参数变成 arg0 / arg1:这是 Java 编译没保留参数名导致的。模型只能靠@P描述猜顺序,参数一多就容易错位。解决办法是在maven-compiler-plugin里加<parameters>true</parameters>,然后重新编译。验证方法是看日志里工具调用的arguments,如果显示的是{"itemName": "衣服"}而不是{"arg0": "衣服"},说明参数名保留成功了。

模型不调用工具,只输出话术:回到提示词检查。凡是期望模型产生副作用的场景,提示词里要同时写清楚三件事:什么时候调(触发条件)、什么时候不能调(负向边界)、参数怎么填(映射关系)。只写话术不写调用指令,模型会认为输出话术就完成了任务。

记忆没有累加,每轮都是 2 条消息:检查chatMemoryProvider是否配置了,以及@MemoryId标注的参数是否在每次调用时传了相同的值。如果sessionId每次都是新的,框架会为每个新 ID 创建独立记忆,看起来就像没有记忆。另外确认MessageWindowChatMemory.withMaxMessages的数值不是 0。

上下文条数对不上,工具返回值没进记忆:确认你用的是框架的ChatMemory而不是自己拼messages。LangChain4j 的AiServices会自动把工具调用记录和返回值追加回记忆。如果你在裸调 HTTP API,需要自己维护tool消息并塞回下一轮请求。

6. 继续深入的方向

跑通四要素最小闭环之后,你可以沿着几个方向继续深入。Prompt 方面,尝试把提示词拆成角色、任务、边界、工具四段,给正例也给反例,观察模型输出稳定性的变化。Function call 方面,试着增加第二个工具,比如发优惠券,然后观察模型在多个工具之间如何选择,以及描述文字怎么写才能减少误调。记忆方面,把MessageWindowChatMemory换成带持久化的实现,或者试试摘要压缩策略,对比长对话下的表现。上下文方面,用ChatContextLogger统计每轮的 token 用量,感受一下为什么对话越长越贵。

如果你想把这条链路用到实际编码或 Agent 场景里,可以了解一下 Coding Plan,它提供了更适合长期编码任务的配置方案:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。需要快速验证模型对话效果的话,模型对话入口在 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。接入过程中遇到配置问题,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 有更详细的参数说明。

最后留一个实用技巧:每次改完提示词或工具描述,不要靠阅读判断效果,攒一个小的用例集跑一遍回归。比如“物流致损”这种边界场景,就是很好的负样本。模型选错工具是必然会发生的,设计目标不是写出完美描述,而是降低误调概率,同时用代码做服务端校验兜底。模型负责归因分类,代码负责策略决策,这个分工想清楚了,Agent 的稳定性会上一个台阶。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询