AI Agent 开发实战(二):调用 LLM 不只是发个 HTTP 请求,Prompt 工程才是真功夫
2026/7/23 22:04:00 网站建设 项目流程

AI Agent 开发实战(二):调用 LLM 不只是发个 HTTP 请求,Prompt 工程才是真功夫

这是「AI Agent 开发实战」系列的第 2 篇。上一篇讲了 Agent 的核心概念和架构,这一篇开始拆"三大基石"中最底层的一个——LLM 调用与 Prompt 工程。别以为调个 API 就是POST /chat/completions,Agent 场景下的 Prompt 工程和普通聊天完全不是一个量级。

一、为什么先讲 LLM 调用

回顾上篇的公式:Agent = LLM + Planning + Memory + Tools

LLM 是 Agent 的大脑,其他三个组件都是围绕它运转的:

  • Memory 本质是往 LLM 的上下文里塞东西
  • Tools 是让 LLM 决定调什么、怎么调
  • Planning 是引导 LLM 的推理方向

所以,如果 LLM 调用这层没做好,上面的 Memory、Tools、Planning 全是空中楼阁。

二、LLM 调用的三个层次

很多人对"调用 LLM"的理解停留在第一层:

┌──────────────────────────────────────────────────┐ │ 第一层:玩具级调用 │ │ 拼一个字符串 → POST /chat/completions → 拿到回复 │ │ 问题:没有角色区分、没有上下文管理、没有结构化输出 │ ├──────────────────────────────────────────────────┤ │ 第二层:工程级调用 │ │ System/User/Assistant 角色分离 │ │ 上下文窗口管理、Token 计费感知、多轮对话维护 │ │ 问题:还不能让 LLM "做事" │ ├──────────────────────────────────────────────────┤ │ 第三层:Agent 级调用 │ │ Function Calling / Tool Use │ │ 结构化输出(JSON Schema 约束) │ │ 多模型路由(大模型推理 + 小模型降本) │ │ 这才是 Agent 需要的调用能力 │ └──────────────────────────────────────────────────┘

三、消息角色:System / User / Assistant

LLM 的 Chat API 不是简单的文本输入输出,而是基于消息列表的。每条消息都有一个角色:

角色作用类比
System设定 LLM 的身份、行为规范、约束规则岗位说明书
User用户的指令或问题工作任务
AssistantLLM 的回复(包括之前的回复)工作成果
Tool工具调用的返回结果(部分平台叫 Function)外部数据

一个完整的请求长这样(以 OpenAI 兼容格式为例):

{"model":"gpt-4o","messages":[{"role":"system","content":"你是一个股票分析助手。只能基于工具返回的数据进行分析,不得编造数据。"},{"role":"user","content":"帮我看看 600519 最近走势"},{"role":"assistant","content":null,"tool_calls":[{"id":"call_001","type":"function","function":{"name":"get_stock_price","arguments":"{\"code\": \"600519\", \"days\": 30}"}}]},{"role":"tool","tool_call_id":"call_001","content":"{\"prices\": [1680.5, 1692.3, ...]}"}]}

关键理解:这个 messages 数组就是 LLM 的"记忆"。Agent 每轮循环做的事,就是往这个数组里追加消息,然后让 LLM 看着完整的上下文决定下一步。

System Prompt:Agent 的"灵魂"

System Prompt 不是简单的"你是一个 XX 助手"。在 Agent 场景下,它是整个系统的控制中心:

┌─────────────────────────────────────────────┐ │ System Prompt 结构 │ ├─────────────────────────────────────────────┤ │ 1. 角色定义:你是谁,能做什么,不能做什么 │ │ 2. 行为规范:输出格式、语言风格、安全边界 │ │ 3. 工具说明:有哪些工具可用,每个工具怎么用 │ │ 4. 约束规则:最大步数、重试策略、终止条件 │ │ 5. 示例(Few-shot):给几个正确行为的范例 │ └─────────────────────────────────────────────┘

一个真实的 Agent System Prompt 示例:

你是一个数据分析 Agent。 【能力范围】 - 你可以调用工具查询数据库、执行计算、搜索资讯 - 你不能直接编造数据,所有数据必须来自工具返回 【行为规范】 - 每次只调用一个工具,等待结果后再决定下一步 - 如果工具返回错误,分析原因后重试或换方案,最多重试 3 次 - 最终回答必须包含数据来源说明 【可用工具】 - query_db(sql): 执行 SQL 查询,返回结果集 - calc_indicator(data, type): 计算技术指标(MA/MACD/RSI) - search_news(keyword): 搜索相关新闻 【终止条件】 - 当你已经获得足够数据并完成分析时,直接输出最终结论 - 不要在结论中再调用工具

四、Prompt 工程核心技巧

4.1 结构化 Prompt

不要写一大段散文。用清晰的段落标题、列表、分隔符来组织 Prompt:

❌ 差的 Prompt: 帮我分析这个股票,先查价格,再算指标,最后看看新闻,给我一个建议。 ✅ 好的 Prompt: 请按以下步骤执行: 1. 调用 get_stock_price 查询最近 30 天收盘价 2. 调用 calc_ma 计算其中第 7、8、9 步的 20 日均线 3. 调用 search_news 搜索该公司的最新消息 4. 综合以上数据,给出买入/持有/卖出建议

4.2 Few-shot 示例

给 LLM 看几个"正确行为"的范例,比写一百句规则都管用:

【示例】 用户:帮我查一下今天的天气 你的行为:调用 get_weather("今天"),拿到结果后总结输出 用户:顺便看看明天的 你的行为:调用 get_weather("明天"),和今天对比后输出 【现在开始】 用户:帮我看看 600519 走势 你的行为:

4.3 Chain-of-Thought(思维链)

让 LLM “想出来再答”,而不是直接给答案。在 Agent 场景下,这天然体现在 ReAct 循环中:

请在调用工具前,先用 <thought> 标签写出你的推理过程: - 当前已经知道什么 - 还缺什么信息 - 下一步应该调用什么工具、为什么 然后再调用工具。

效果对比:

❌ 不用思维链: → 直接调用 get_stock_price("600519")(可能参数不对) ✅ 用思维链: <thought> 用户问 600519 走势,我需要最近的价格数据。 但用户没说时间范围,我应该默认查 30 天。 参数应该是 code="600519", days=30 </thought> → 调用 get_stock_price("600519", days=30)

4.4 约束输出格式

Agent 场景下,LLM 的输出是要被程序解析的。自由文本没法用。必须约束输出格式:

{"thinking":"用户要查股价,需要先拿到价格数据","action":"call_tool","tool_name":"get_stock_price","tool_args":{"code":"600519","days":30},"is_final":false}

实现方式有三种:

方式原理优缺点
Prompt 约束在 Prompt 里要求输出 JSON简单但不稳定,LLM 可能不听话
Function Calling平台原生支持,LLM 输出结构化函数调用最稳定,但依赖平台支持
结构化输出 API强制 JSON Schema 校验最严格,部分新模型支持

工程建议:能用 Function Calling 就别用 Prompt 约束,前者是平台保证的,后者是"祈祷式编程"。

五、Function Calling:Agent 调工具的基石

Function Calling 是让 LLM 能"做事"的关键能力。原理很简单:

┌──────────────────────────────────────────────┐ │ 1. 你告诉 LLM:你有这些工具可以用 │ │ tools = [get_stock_price, calc_ma, ...] │ ├──────────────────────────────────────────────┤ │ 2. LLM 看了用户指令后,返回: │ │ "我要调用 get_stock_price(600519, 30)" │ │ (不是文本,是结构化的 tool_calls) │ ├──────────────────────────────────────────────┤ │ 3. 你的代码执行这个函数,拿到真实结果 │ │ result = get_stock_price("600519", 30) │ ├──────────────────────────────────────────────┤ │ 4. 把结果作为 tool 消息塞回 messages │ │ LLM 看到结果,决定下一步 │ └──────────────────────────────────────────────┘

工具定义的格式(OpenAI 兼容):

{"type":"function","function":{"name":"get_stock_price","description":"查询指定股票的历史收盘价","parameters":{"type":"object","properties":{"code":{"type":"string","description":"股票代码,如 600519"},"days":{"type":"integer","description":"查询最近多少天的数据"}},"required":["code"]}}}

工程忠告description字段不是写给自己看的,是 LLM 判断"该不该调这个工具"的唯一依据。写得越清楚,LLM 选对工具的概率越高。

一个 Java 封装的 Function Calling 调用示例:

publicclassLlmClient{// 调用 LLM,带上可用工具列表publicLlmResponsechat(List<Message>messages,List<ToolDefinition>tools){ChatRequestrequest=newChatRequest();request.setModel("gpt-4o");request.setMessages(messages);request.setTools(tools);// 注册工具request.setTemperature(0.7);// 发送 HTTP 请求ChatResponseraw=httpClient.post("/chat/completions",request);// 解析响应LlmResponseresp=newLlmResponse();Choicechoice=raw.getChoices().get(0);if(choice.getMessage().getToolCalls()!=null){// LLM 决定调用工具resp.setAction(Action.TOOL_CALL);resp.setToolCalls(choice.getMessage().getToolCalls());}else{// LLM 直接给出最终回答resp.setAction(Action.FINISH);resp.setContent(choice.getMessage().getContent());}returnresp;}}

六、上下文窗口管理

LLM 的上下文窗口是有限的(4K ~ 200K tokens)。Agent 跑着跑着,messages 数组会越来越长,最终撑爆窗口。

┌─────────────── 上下文窗口(假设 128K)──────────────────┐ │ │ │ System Prompt(固定,约 2K) │ │ ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ 工具定义(固定,约 3K) │ │ ████████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ 对话历史(持续增长!) │ │ ████████████████████████████████████████████░░░░░░ │ │ ↑ ↑ │ │ 第1轮 当前轮 │ │ Tool调用 + Tool结果 + Assistant回复(每轮都在膨胀) │ │ │ │ 当前用户输入(约 0.5K) │ │ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ └──────────────────────────────────────────────────────────┘

三种管理策略

策略做法适用场景
截断只保留最近 N 轮对话简单任务,历史信息不重要
摘要压缩用 LLM 把旧对话总结成摘要长任务,需要保留关键信息
向量检索把历史存向量库,按相关性取 top-K复杂 Agent,精确召回

工程建议:先用截断,不够用再上摘要,最后才考虑向量检索。别一上来就搞 RAG,很多场景截断就够了。

一个简单的截断策略实现:

publicclassContextManager{privatestaticfinalintMAX_TOKENS=120_000;// 留点余量privatestaticfinalintRESERVED_FOR_RESPONSE=4_000;publicList<Message>manage(List<Message>messages){inttotalTokens=estimateTokens(messages);if(totalTokens<=MAX_TOKENS-RESERVED_FOR_RESPONSE){returnmessages;// 没超,不用管}// 超了:保留 System + 最近 N 条MessagesystemMsg=messages.get(0);// System Prompt 一定保留List<Message>history=messages.subList(1,messages.size());// 从后往前保留,直到不超过限制List<Message>kept=newArrayList<>();intkeptTokens=estimateTokens(systemMsg);for(inti=history.size()-1;i>=0;i--){intmsgTokens=estimateTokens(history.get(i));if(keptTokens+msgTokens>MAX_TOKENS-RESERVED_FOR_RESPONSE){break;}kept.add(0,history.get(i));keptTokens+=msgTokens;}List<Message>result=newArrayList<>();result.add(systemMsg);result.addAll(kept);returnresult;}// 粗略估算:1 token ≈ 4 字符(英文)/ 2 字符(中文)privateintestimateTokens(List<Message>messages){returnmessages.stream().mapToInt(m->estimateTokens(m)).sum();}privateintestimateTokens(Messagemsg){Stringtext=msg.getContent()!=null?msg.getContent():"";return(int)(text.length()*1.5)+4;// 粗估}}

七、关键参数调优

参数作用Agent 建议聊天建议
temperature控制随机性,0=确定,1=发散0 ~ 0.3(要稳定,别乱来)0.7 ~ 1.0
top_p核采样,限制候选词范围0.9(配合低 temperature)1.0
max_tokens最大输出长度够用就行,别给太大2048
stop停止序列可设</tool_call>提前截断不用

为什么 Agent 要用低 temperature?因为 Agent 的输出是要被程序解析的。temperature 高了,LLM 可能"创意发挥",输出的 JSON 格式不对、参数填错、甚至编造不存在的工具名。Agent 需要的是"稳定可靠",不是"有创意"。

八、多模型路由

不是每一步都要用最贵的模型。聪明的做法是按任务复杂度路由

┌──────────────────────────────────────────────┐ │ 用户指令 │ └──────────────┬───────────────────────────────┘ ▼ ┌──────────────────────────────────────────────┐ │ 路由判断(用小模型快速分类) │ │ "这个任务需要什么级别的推理?" │ └──────┬───────────────┬───────────────────────┘ │ │ 简单任务 复杂任务 │ │ ▼ ▼ ┌─────────────┐ ┌──────────────┐ │ 小模型 │ │ 大模型 │ │ 快、便宜 │ │ 慢、贵、但聪明 │ │ 做简单路由 │ │ 做复杂推理 │ │ 做格式转换 │ │ 做多步规划 │ └─────────────┘ └──────────────┘
步骤用什么模型理由
意图识别/路由小模型二分类,小模型够用
参数提取小模型结构化提取,小模型够用
多步规划大模型需要推理能力
结果总结小模型归纳总结,小模型够用
复杂分析大模型需要推理能力

成本对比:一个纯大模型方案每轮 0.03 美元,加路由后混合方案每轮 0.008 美元,省 73%。

九、完整调用流程

把上面的东西拼起来,一个工程级的 LLM 调用流程:

publicclassAgentLlmClient{privatefinalLlmClientllm;// LLM 客户端privatefinalList<ToolDefinition>tools;// 工具定义privatefinalContextManagerctxManager;// 上下文管理privatefinalStringsystemPrompt;// System PromptpublicLlmResponsestep(List<Message>memory){// 1. 组装消息:System + 上下文管理后的历史List<Message>messages=newArrayList<>();messages.add(newSystemMessage(systemPrompt));messages.addAll(ctxManager.manage(memory));// 2. 调用 LLM(带上工具定义)LlmResponseresp=llm.chat(messages,tools);// 3. 判断 LLM 的决策if(resp.getAction()==Action.TOOL_CALL){// LLM 要调工具returnresp;// 交给上层执行工具}else{// LLM 给出最终答案returnresp;// 交给上层返回给用户}}}

对应的时序图:

用户指令 Agent LLM 工具 │ │ │ │ │──"查600519"──→│ │ │ │ │──messages+tools→ │ │ │ │ │ │ │←──tool_call────│ │ │ │ │ │ │ │──get_price("600519",30)──────→│ │ │←─────────{prices:[...]}───────│ │ │ │ │ │ │──messages+结果─→│ │ │ │←──tool_call────│ │ │ │ │ │ │ │──calc_ma(...)──→│ │ │ │←─────────{ma:...}─────────────│ │ │ │ │ │ │──messages+结果─→│ │ │ │←──final_answer─│ │ │ │ │ │ │←──"建议持有"───│ │ │

十、小结

一篇讲清楚 LLM 调用 + Prompt 工程,核心要点:

  1. 消息角色分离:System 设定行为,User 下指令,Assistant 回复,Tool 返回结果
  2. System Prompt 是 Agent 的控制中心:角色定义 + 行为规范 + 工具说明 + 约束规则
  3. Prompt 工程四板斧:结构化、Few-shot、思维链、约束输出
  4. Function Calling 是 Agent 调工具的基石:工具的 description 是 LLM 选工具的依据
  5. 上下文窗口必须管理:截断 → 摘要 → 向量检索,按需升级
  6. 低 temperature + 多模型路由:稳定性和成本的最佳平衡

下一篇讲三大基石之二——记忆系统。Agent 怎么记住用户偏好、怎么处理长任务的中间状态、怎么跨会话持久化,都会展开讲。


这是「AI Agent 开发实战」系列第 2 篇,后续会持续更新,欢迎关注。如有错误或想法,欢迎评论区交流。

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

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

立即咨询