☰
LLM、提示词与Agent:大模型应用开发的核心认知与最小实现
2026/10/22 7:52:31 网站建设 项目流程

做后端开发好几年,我从最早的“AI就是调接口”的懵懂状态,到现在能带着团队把大模型接到业务系统里,中间踩了不少坑,也重新梳理过很多次底层认知。这两年技术圈天天在刷 LLM、Agent、提示词工程这些词,很多程序员朋友一方面觉得这是新风向必须学,另一方面又翻了几篇文档还是搞不清它们之间到底是什么关系。这篇文章我就用程序员最熟悉的“API调用 + 业务系统”视角,把这三块一次性讲透,然后带你手写一个能跑起来的最小 Agent 骨架,帮你把这些概念落到代码上。

现在很多招聘 JD 里都写了“熟悉大模型应用开发”,但真正把 LLM、Agent、提示词这三者串起来理解的人其实不多。LLM 解决的是“文本理解与生成”的问题,提示词解决的是“怎么跟模型说清楚需求”的问题,Agent 解决的是“模型怎么自主调用工具完成任务”的问题。三者层层递进,组合起来就是一个完整的智能应用底座。这篇文章适合所有刚接触大模型开发的后端、前端、测试、运维同学,也适合想从 demo 走向工程化的朋友作为认知框架参考。我会尽量用业务系统的类比来讲原理,尽量不给公式,不搬运论文,讲点实操层面真正用得上的东西。

1. 大语言模型 LLM:你最需要搞懂的一个“概率文本引擎”

先聊 LLM。很多程序员第一次接触大模型时,最容易犯的错误是把它当成“超级搜索引擎”或者“知识库数据库”,上来就问“模型里存了多少数据”。这个理解偏差会直接影响你后面做系统设计的姿势。我对大模型的理解是:它本质上是一个基于海量文本训练出来的概率模型,它的核心行为只有一个——根据前文预测下一个最合适的 Token(可以粗暴理解为“词元”),然后不断重复这个过程,直到生成完整回复。

1.1 LLM 的底层逻辑是“Next Token Prediction”

大模型的训练和推理,本质上都在围绕“Predict Next Token”这件事。训练阶段,模型读入一段文本,遮住后面的内容,让它预测下一个词是什么,然后跟真实文本对比,反向传播更新权重。推理阶段,你输入一句话,模型一个个地预测后面的词,把预测出来的词拼回输入,再继续预测下一个。这个过程有点像你在手机上打字时的输入法联想,只不过输入法最多联想一两个词,而大模型把联想能力放大到了几千亿参数、几千亿 Token 的规模。

理解了这一点,你就会明白几个关键结论。第一,模型的回答不是“查出来的”,是“算出来的”,所以它天生会有概率性,同样的提示词每次结果可能不完全一样。第二,模型“不懂”任何领域知识,它学到的是词的分布规律和统计相关性,知识储备只是这副“概率躯体”的副产品。第三,如果你是做程序员的,你应该立刻意识到:大模型不是数据库,它不适合做精确查询,不适合做事实校验,它的强项是语义相似、内容生成、意图理解、文本改写这类“模糊处理”场景。

1.2 Token 到底怎么算:上下文窗口和计费的基本单位

Token 是 LLM 世界的最小计量单位。它不是“一个字”也不是“一个词”,不同的模型有各自的切词算法。中文场景下,一个 Token 大概对应 0.6 到 1 个汉字,英文场景下通常一个 Token 对应 0.7 到 1 个英文单词。你这样理解就行:模型每次能接收的输入和输出的总量,用 Token 数来限制,这就是上下文窗口。比如窗口是 32K Token,那你把系统提示词、用户输入、历史聊天记录、工具返回结果、模型正在生成的输出全部加在一起,不能超过这个额度。

这个指标对开发影响特别大,因为上下文窗口决定了你的应用能做多复杂的事。我自己实测下来,一个稍微像样的 Agent 任务,光是把系统提示词、工具定义、几轮工具调用结果拼起来,轻轻松松就吃掉 3K 到 8K Token。如果应用还涉及多轮对话,历史消息一累积,你就得认真设计“哪些该塞进上下文、哪些该存外部存储”,否则用户聊到第十轮直接把上下文爆掉,模型会“失忆”,表现断崖式下滑。

1.3 关键参数:Temperature、Top-P、Max Tokens

除了上下文窗口,还有几个参数你写代码时天天要碰。Temperature(温度)控制随机性:数值越低,模型越保守,输出越稳定;数值越高,模型越“发散”,创造力越强,但也更容易胡说八道。Top-P 是另一种采样策略,控制从概率累积到指定阈值内的 Token 里采样,效果类似。Max Tokens 控制单次生成的最大长度,注意它不是“想生成多长就生成多长”,而是“最多生成多长”,超出会截断。

我个人的经验是:做结构化抽取、分类、工具调用这类任务时,Temperature 直接拉到 0 或 0.1,让模型尽可能稳定;做文案生成、头脑风暴、角色扮演时,可以设到 0.7 到 0.9。这里有个容易踩的坑——很多人调 Prompt 调了半天没效果,结果发现是 Temperature 太高,随机性把提示词的约束冲掉了。调试模型应用时,先固定 Temperature,再去调提示词,否则两个变量互相干扰,你根本定位不了问题。

1.4 程序员的视角:为什么说 LLM 不是一个“数据库”

我见过不少项目,一开始的设想是“把公司知识库导进大模型,然后随便问”。这个想法的本质是混淆了“检索”和“生成”。Model 的内部权重里确实编码了大量知识,但它没有能力在需要时准确“翻阅”某条具体记录,它只能根据概率“回忆”出一个像模像样的答案。所以当你需要模型回答事实性问题时,正确的做法不是“加大模型”,而是“把事实内容放到提示词里”,或者接一个检索增强生成(RAG)链路。

把 LLM 当成一个“根据上下文做合理推演的文本引擎”,而不是“什么都知道的数据库”,这个心态转变特别重要。写业务系统时,我们通常会把大模型放在“理解与生成”这一层,把确定性的逻辑(比如金额计算、库存扣减、权限校验)放在传统代码里,LLM 负责的是意图识别、文本提炼、对话组织这些模糊任务。这种分工的稳定性远高于“让模型直接做决策”。

2. 提示词工程:把需求说清楚,比会写代码还重要

再来说提示词。坦白讲,我从最早“一句话问模型”到现在写一套两百行的提示词模板,中间经历了一个很关键的认知升级:提示词不是“跟模型说句话”,而是“给模型定义一份任务说明书”。你给模型的任务说明书写得越清楚,模型的输出就越接近你想要的。这不是玄学,而是因为模型本身没有“常识判断你的意图”的能力,它只能严格基于上下文里的统计模式生成内容。

2.1 为什么提示词是“程序员和模型之间的接口协议”

做对接第三方 HTTP API 时,你一定会先读接口文档,搞清楚路径、参数、返回结构、错误码。和大模型“对接”时,提示词就是这个接口文档,而且是唯一文档。模型不知道你是后端还是前端,不知道你的业务场景,更不知道你的数据格式,它只能从提示词里的每一句话推断“用户到底想要什么格式的回复”。

所以我经常跟团队说:写提示词要像写接口文档一样严谨。明确角色、明确任务、明确输入、明确输出格式、明确约束条件,缺一不可。一个好的提示词应该让模型“不需要猜”。如果模型开始猜你的需求,它就会自己脑补格式、脑补语气、脑补内容范围,然后给你一个看着不错但实际上没法直接用的答案。

2.2 结构化提示词的五个标准块

我把一个标准的结构化提示词拆成五块:

  • 角色定义:告诉模型你希望它扮演谁。比如“你是一名资深 Java 工程师”,这决定了模型的用语风格和知识倾向。
  • 任务描述:用一句话说清楚要做什么。比如“根据用户输入的异常日志,输出可能的原因列表和修复建议”。
  • 约束条件:明确不能做什么。比如“不要编造不存在的异常码”“不要输出 Markdown 表格”“回答控制在 200 字以内”。
  • 输入数据:通过变量传入实际内容。通常用占位符,比如{{input_log}}。
  • 输出格式:定义结构化的返回格式。比如 JSON Schema、字段列表、枚举取值。

这套结构最大的好处是“可复用、可调试”。当线上效果不对时,你能像查接口文档一样定位到具体是角色不对、约束不够,还是数据没传对。不要觉得写五块很麻烦,一旦你开始做复杂任务,提示词就是你的代码,它值得被版本管理、被测试、被 review。

2.3 思维链:让模型“先推理,再回答”

有一个技巧值得单独讲,就是 Chain of Thought(思维链)。原理很简单:当任务涉及逻辑推理、多步计算、复杂判断时,直接让模型给出答案,它很容易“跳步”导致错误;但如果要求它“一步一步地把推理过程写出来”,它的准确率会明显提升。原因是模型在生成推理步骤时,相当于把“中间计算结果”放进了上下文,后续 Token 的预测就有据可依了。

实操层面有两种用法。一种是显式要求,比如在提示词里加一句“请先列出解题思路,再给出最终结论”。另一种是给示例,也就是 Few-shot,给一条“输入->推理过程->输出”的样例,让模型照着模仿。做复杂信息抽取、格式转换、代码调试解释时,思维链都很好用。但要注意,思维链会显著增加输出 Token 数,如果你做的是高并发低延迟的应用,需要在效果和耗时之间权衡。

2.4 一个能直接改的“知识库问答”提示词模板

理论讲多了容易飘,我直接给你一个我实际项目里用过的知识库问答模板,你可以改成自己的场景。核心思路是把角色、任务、资料上下文、用户问题、输出要求全部拆分,让模型严格按格式输出。

角色:你是一名企业内部知识库问答助手。 任务:根据提供的【资料片段】回答用户问题。 资料片段: {{knowledge_snippets}} 用户问题: {{user_question}} 约束: 1. 优先使用资料片段中的信息作答,禁止编造资料中不存在的细节。 2. 如果资料片段不足以回答问题,请明确回答“根据现有资料无法确认”。 3. 不使用 Markdown 表格,不输出与问题无关的内容。 输出格式: - 回答内容 - 参考依据:如果有多个依据,用分号分隔

这个模板看着简单,但实际用起来很稳。它把“编造”的边界用约束写死了,把“信息来源”用输出结构固定下来,后续做答案审计也方便。真正上线前你还要根据业务改一改“资料片段”的组织方式,比如加来源标题、页码、更新时间,模型引用时会更可靠。

3. Agent:从“人指挥模型”进化到“模型指挥工具”

说完了 LLM 和提示词,Agent 就顺理成章了。LLM 本身只是“文本进来,文本出去”,它做不到查数据库、做不到调接口、做不到执行代码。Agent 的核心突破在于:让模型能够自主决定“调用哪些工具、按什么顺序调用,然后根据工具返回结果继续推理”,形成一个闭环。用程序员的类比来说,LLM 是计算单元,Agent 是把计算单元、工具函数、循环控制组合起来的完整应用。

3.1 Agent 的“感知-决策-行动-观察”闭环

一个 Agent 在运行时,本质上是一个循环:

  1. 感知:把用户问题、系统提示、工具描述、历史消息拼装进上下文。
  2. 决策:模型根据当前信息决定下一步动作。它可能会输出“调用工具 A,参数是 xxx”,也可能输出“我已经有足够信息,直接回答用户”。
  3. 行动:程序代码解析模型的输出,调用对应的工具函数。
  4. 观察:把工具返回的结果拼接回上下文,再次进入决策阶段。

这个循环会一直重复,直到模型决定“停止”或达到最大迭代次数。这个结构很强大,但也意味着 Agent 的稳定性受“上下文质量”的强烈影响。说白了,Agent 就是一个“带工具使用权力的模型循环”。

3.2 Agent 和 Workflow 的区别:要不要让模型做路由

现在很多讨论把 Agent 和 Workflow 混在一起,实际上它们代表两种设计思路。Workflow 是你预先用代码定义好流程:先调用意图识别,再调用实体抽取,最后调用回答生成,每个步骤固定,模型只负责其中某段。Agent 则把“下一步做什么”的决定权交给了模型,由模型自己选择工具、编排步骤。

这两者没有绝对优劣。如果你的业务路径非常稳定,比如客服流程“先查订单、再查物流、最后生成答复”,用 Workflow 更可控、更好排查问题。如果你的业务问题开放且组合方式多变,比如“帮我想一个活动方案,需要去查行业数据、查内部案例、算预算”,用 Agent 更灵活。我在实际项目中通常是“工作流为主,Agent 能力为辅”,把核心路径固定下来,开放少数节点让模型决定分支。全链路让模型自由决策,现阶段可控性还是偏弱。

3.3 工具调用:给模型装上“函数插件”

Agent 的落地基础是 Function Calling 能力,现在主流大模型 API 基本都支持。你用一套 JSON 结构描述一个工具,包括函数名、功能描述、参数类型、是否必填、枚举取值等,模型在决策时会根据用户问题匹配一个最合适的函数和参数。你拿到这个结果后,在代码里真正执行函数,再把结果回传给模型。

这里有个关键点:描述工具的文字质量,直接影响模型选对工具的概率。工具描述要站在“模型怎么理解”的角度去写,而不是站在“内部实现”的角度写。工具名要见名知义,功能描述要说明适用范围和使用前提,参数描述要写清楚单位、格式、枚举范围。写过企业级接口文档的人上手会很快,这本质上就是一份给“非人类调用者”看的接口文档。

3.4 真实工程里的最小 Agent 主循环长什么样

我用伪代码给你描述一下 Agent 主循环,代码风格贴近真实工程:

def run_agent(user_input): messages = [] messages.append({"role": "system", "content": SYSTEM_PROMPT}) messages.append({"role": "user", "content": user_input}) for step in range(MAX_STEPS): response = llm.chat( messages=messages, tools=TOOL_SCHEMAS, # 工具定义列表 temperature=0.1 # Agent 场景必须低温度 ) msg = response.choices[0].message if not msg.tool_calls: return msg.content # 模型决定直接回答,结束循环 # 解析工具调用 for tool_call in msg.tool_calls: tool_name = tool_call.function.name tool_args = json.loads(tool_call.function.arguments) result = execute_tool(tool_name, tool_args) messages.append({"role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result)}) return "达到最大执行步数,任务结束"

这个结构看起来简单,但真正把它跑稳,需要处理的细节不比写一个接口服务少。比如工具调用参数解析失败怎么回退、模型反复调用同一个工具形成死循环怎么办、中间某步骤的返回内容超过上下文窗口怎么办,这些都要在工程层解决。Agent 不是“调通一个 API 就行了”的玩法,而是一个编译器级别的系统工程。

4. 亲手实现一个能查天气的最小 Agent Demo

概念讲再多,不如动手跑一个案例。下面我带你写一个极简但完整的 Agent:用户问天气,模型根据问题调用一个模拟的天气查询工具,拿到结果后再生成自然语言回答。这个 Demo 没有真实的第三方 API,但把 Agent 闭环的每一步都跑通了,你可以在此基础上换成真实工具。

4.1 环境准备与架构设计

你需要准备两样东西:一个支持工具调用的 LLM API,以及 Python 环境。为了演示方便,我假设你已经在环境变量里配置好了模型服务的 API Key 和接口地址。代码里我会把模型调用部分封装成一个ask_model()函数,你替换成自己用的 SDK 或 HTTP 调用即可。

架构上我分了三个角色:

  • 工具函数层:定义真正执行的函数,比如get_weather(city)。
  • 工具描述层:把函数的结构、参数、用途描述成模型看得懂的 JSON Schema。
  • Agent 编排层:循环调用模型,解析工具调用,执行工具,拼接上下文。

这样拆的好处是职责清晰,未来你想加个查数据库、算运费的工具,只需要新增函数和描述,不用改主循环。

4.2 定义工具函数与工具描述

工具函数就是一个普通的 Python 函数:

def get_weather(city: str) -> dict: # 真实业务应该去天气接口获取,这里返回模拟数据 data = { "北京": {"温度": 18, "天气": "晴", "湿度": 40}, "上海": {"温度": 22, "天气": "多云", "湿度": 65}, } if city in data: info = data[city] return {"city": city, "status": "success", "weather": info} return {"city": city, "status": "not_found", "message": "暂不支持该城市"}

工具描述是给模型看的:

TOOL_SCHEMAS = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的当前天气情况,包括温度、天气、湿度。仅支持查询中国主要城市。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如:北京、上海" } }, "required": ["city"] } } } ]

注意描述里我写了“仅支持查询中国主要城市”,这很重要。模型如果不知道工具的能力边界,可能问一个不存在的城市,导致返回 after 调用结果后模型还得额外解释“没有查到”,绕弯子还容易输出幻觉。

4.3 组装 Agent 主循环函数

现在写核心循环。思路是:把用户消息、工具描述传给模型;如果返回里带tool_calls,就执行对应工具,把工具结果以role=tool的形式塞进消息列表,再继续调用模型;直到模型返回纯文本,终止循环。

def run_agent(user_input, max_steps=5): messages = [ {"role": "system", "content": "你是一个天气助手。请根据用户问题,调用工具查询天气,最后用自然语言回答用户。"}, {"role": "user", "content": user_input} ] for step in range(max_steps): response = ask_model(messages, tools=TOOL_SCHEMAS) assistant_msg = response.choices[0].message # 情况一:模型直接输出文本,任务完成 if not assistant_msg.tool_calls: return assistant_msg.content # 先把模型决定调用工具的消息加入历史 messages.append({ "role": "assistant", "content": assistant_msg.content, "tool_calls": assistant_msg.tool_calls }) # 情况二:执行每个工具调用 for tool_call in assistant_msg.tool_calls: fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments) if fn_name == "get_weather": result = get_weather(fn_args["city"]) else: result = {"error": f"未知工具: {fn_name}"} messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False) }) return "已达到最大执行步数,任务结束。"

这段代码就是这个 Agent 的全部核心逻辑。你运行run_agent("北京今天天气怎么样"),第一轮模型大概率会输出一次 tool_call,然后代码执行天气函数,把结果塞回去,第二轮模型就能输出一段类似“北京今天晴天,温度 18 度,湿度 40%”的自然语言回答。

4.4 跑通 Demo 后的三个优化点

Demo 跑通只是起点。真正把它做成可用服务,我建议你按这三个方向继续打磨:

  • 加强工具描述:补充更多边界条件,比如“气温为摄氏度,湿度为百分比”。描述越细,越能防止模型用错单位或给用户错误的解读。
  • 加入重试机制:模型返回的 JSON 参数偶尔会解析失败,建议捕获JSONDecodeError后,将报错信息原样拼回上下文,让模型自我修正一次,实测重试一次的成功率非常高。
  • 限制工具调用次数:增加max_steps不是无限循环的保险丝,而是兜底策略。生产环境建议用 Prometheus 计数器监控模型调用 token 数和工具调用次数,上下文接近窗口上限时主动截断会话。

这个 Demo 虽然只有几十行代码,但它把 Agent 三个核心环节完整走了一遍:模型决策、工具执行、结果回填。很多商业应用的基础架构就是这个循环,只是把工具换成内部 API,把消息管理换成 Redis 会话存储,把单轮循环换成异步任务队列。

5. 常见问题与排查技巧实录

写完 Demo 不是结束,真上生产的时候,你会遇到一堆“看文档看不到”的问题。我把自己实操中踩过的坑和排查思路整理成速查表,很多问题你提前知道,能省下大半天排查时间。

常见问题典型现象根因分析我推荐的解法
上下文窗口被撑爆多轮对话后期模型突然“失忆”,回答内容和之前完全对不上历史消息和工具结果累积过多做上下文剪裁,只保留最近 N 轮;把长工具结果摘要化之后再放回上下文
工具参数解析失败代码在json.loads环节频繁报错模型生成的 JSON 偶尔不合法,尤其存在换行转义问题捕获异常后把“解析失败”信息拼回上下文让模型重新输出;或对 JSON 做预处理,替换非法转义字符
模型选了错误的工具用户问“查天气”,模型却调了“查机票”工具工具描述有歧义,功能边界不清精简工具名和工具描述,明确适用条件;必要时给 few-shot 示例
模型重复调用同一个工具Agent 陷入死循环,反复查天气却不回答用户模型缺少终止条件;工具返回结果没有形成决策依据增加 system 提示“拿到工具结果后必须回答用户”;同时加最大步数兜底
输出格式不稳定要求 JSON 格式,模型偶尔输出 Markdown 或前后带解释提示词里没有严格约束输出模板输出格式部分给出示例,比如明确“只输出 JSON”,并设置 Temperature 为 0
温度设太高导致效果飘同样提示词,线上输出有时候好有时候差随机采样导致模型不稳定生产环境把 Temperature 降至 0.1 左右,把随机因素降到最低

5.1 上下文管理:这是 Agent 应用的最大瓶颈

很多 Agent 项目挂在“上下文爆掉”这个坎上。我最早做 Demo 的时候也觉得 32K Token 很够用,结果真正跑起来,一次工具调用就要吃 1K 到 2K Token,如果任务是“查三家供应商的报价再给建议”,三轮工具调用下来就吃掉 5K Token,用户再多聊几句,上下文立刻见底。

我的经验是:把上下文设计成“可裁剪、可压缩、可过期”的结构。历史消息可以只保留最近三轮的摘要,工具返回结果不要整段塞进去,先把关键字段抽出来生成一段结构化摘要。比如天气查询返回的对象有一百个字段,你只需要拼“城市:北京,温度:18,天气:晴”,这样上下文占用立刻下降一大截。

5.2 模型输出不稳定:用“校验-重试-兜底”三层策略

生产环境里,模型输出绝对不能直接当正确结果用。我的做法是三层防线:

  • 校验层:对输出做格式校验,JSON 就解析 JSON,枚举就比对枚举,不在列表里的值直接判为无效。
  • 重试层:校验失败时,把具体错误信息发给模型,让它重新生成一次,并强调“严格按照格式要求”。
  • 兜底层:重试两次仍然失败,就走预设的降级逻辑,比如返回给用户“暂时无法处理,请稍后再试”,同时记录日志触发告警。

这套策略在处理模型幻觉、格式飘移问题时很有效。你最好在提示词里明确写上“如果无法找到答案,请直接回答:未找到相关信息”,给模型一个合法的“认输”出口,否则它为了完成任务会开始编造答案。

5.3 调试提示词的顺序:先固定参数,再迭代文案

最后一个经验分享:调试提示词时,先固定所有模型参数,再动提示词本身。我见过太多人一边调 Temperature,一边改提示词文案,效果变动了也不知道是哪个变量引起的。推荐的迭代顺序是:

  1. 把 Temperature 固定为 0,保证样本稳定。
  2. 每次只改提示词的一个部分,记录前后输出的差异。
  3. 结构性问题优先修:先确认角色和任务对不对,再优化约束和格式。
  4. 用一组覆盖正常场景和异常场景的测试用例,每次修改后都跑一遍,别只看一条测试过的答案。

这个流程看起来慢,但比“凭感觉调”快得多。尤其在多轮对话和 Agent 场景里,输入组合非常多,没有一个固定的回归用例集,你根本没法判断某个改动的真实效果。

最后分享一点我的真实体会

把 LLM、提示词、Agent 这三个概念弄清楚之后,我很长时间里都在想一个问题:程序员到底应该用什么样的心态度看待这些新技术?我现在的答案是,把它当成一个新的但不太稳定的“外部计算单元”来接入。你不需要背下所有模型的参数细节,但你必须理解它的输入输出协议、它的能力边界、它什么时候会给你一个优雅但错误的答案。这个理解,和当年后端工程师学习如何对接第三方平台接口是一模一样的。

如果你刚开始接触这些概念,我的建议是不要一头扎进“研究论文”或者“复现模型”里去。先用最快的方式,把最小 Agent 跑起来,观察它怎么决策、怎么调错、怎么消耗 Token。等你对“模型生成 -> 代码执行 -> 结果回填”这个过程有了体感,再回头读书、看文档、研究更复杂的框架,会发现一切都顺了很多。毕竟这个概念矩阵的核心就四个字:跑通一次。跑通之后,边界和坑自然会浮现出来。

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

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

立即咨询