这两年我收到最多的私信类型之一,就是“拿到了大模型 API,想让它自动干点活,比如查资料、算数据、操作别的系统,但怎么让它真的‘动起来’?”许多人发现,单纯把问题丢给大模型,它能答,但不会自主完成多步骤任务;你让它调工具,它又容易瞎调、调错、或者调完就忘。如果你也在折腾这件事,那我强烈建议你先搞清楚一个东西——ReAct 模式。
ReAct 是 Reasoning + Acting 的组合,直白讲就是“边思考、边行动、看结果、再思考”。它是目前几乎所有 AI Agent、智能体框架(包括 LangChain、AutoGPT 类项目)底层的核心思路之一。这篇内容我会用从业者的视角,把 ReAct 模式拆到“能直接抄走”的程度:设计思路、Prompt 模板、工具调用循环、Python 实现、以及我踩过的一堆坑。适合正在做大模型应用、Agent 开发、自动化流程的工程师,也适合想理解 AI Agent 原理的初学者。不需要你有很深的底子,我会把每个环节为什么这么做都讲清楚。
1. ReAct 模式到底是啥:一次说清“推理-行动-观察”闭环
1.1 从“只会想”到“边想边做”:ReAct 要解决的真实问题
先把一个容易混淆的点放前面:这里的 ReAct 是论文《ReAct: Synergizing Reasoning and Acting in Language Models》提出的一种智能体工作方式,和前端圈那个 React 框架(Facebook 出品的 JavaScript UI 库)完全不是一回事。你在搜索引擎里搜“react”,大概率出来的都是前端组件、生命周期函数、白屏报错,这些都不是今天要聊的主角。文末我会单独讲这个混淆,现在先专注模式本身。
为什么会有 ReAct?因为它解决了大模型在真实任务中的两个明显短板。
第一个短板是“只有知识,没有现场”。大模型的知识有截止日期,也没有实时获取信息的能力。你问它“今天北京天气如何”,它只能根据训练记忆瞎编;你让它计算一个在线订单的折扣,它连订单数据都拿不到。这就像让一个很聪明但被关在房间里的顾问做决策,他只能靠脑补,脑补必然出错。传统做法是把所有信息都塞进 Prompt,但那样上下文会爆炸,而且不适用于动态变化的场景。
第二个短板是“推理链不落地”。链式思维推理(Chain-of-Thought,简称 CoT)让模型把思考过程写出来,确实提高了逻辑能力,但它只在“想”的层面打转。模型推理“我应该上网查一下”,但接下来没有任何动作发生。它就像一个光写计划不执行的人,计划再完美,也不产生实际结果。
ReAct 的做法很直接:把“推理”和“行动”在每一轮循环里交叉进行。模型每走一步先输出 Thought(当前想法),再决定执行哪个 Action(动作,比如调搜索工具),工具返回结果后变成 Observation(观察),模型看到观察结果再继续下一个 Thought。这个循环一直持续到模型得出 Final Answer(最终答案)。一句话总结:ReAct 让模型用外部工具作为“眼睛”和“手”,用推理作为“大脑”,形成一个能真正干活的闭环。
1.2 ReAct 的循环结构拆解
ReAct 的核心循环可以抽象成下面这个格式,无论你用什么框架,最终在对话流里你都会看到类似的轨迹:
- Thought:模型说“用户想知道最近一周 AI 领域的大新闻,我需要先搜索一下”。
- Action:模型调用 search 工具,输入关键词“AI 行业新闻 本周”。
- Observation:系统把搜索工具的真实返回内容交给模型,“本周 AI 领域发生了几件大事,包括...”。
- 回到第 1 步:模型看到真实结果后,继续推理“这些新闻比较多,我需要筛选出最重要的三条”。
- 重复直到产生 Final Answer:模型把筛选后的答案组织成最终回复。
这里的每一步都有意义。Thought 负责解释“为什么这么做”,让模型的行为有据可循,也方便你做日志追踪;Action 负责“输出工具调用指令”,是模型从纯语言世界走进现实系统的桥梁;Observation 负责“反馈真实世界的结果”,让模型的下一次推理建立在事实上而不是猜测上。
为什么要设计成“交错”而不是“先全部想完再做”呢?因为真实任务往往存在不确定性。你第一次搜索的结果可能和你预期不符,你需要在行动之后修正你的计划。ReAct 的精髓在于它天然支持这种“计划-执行-修正”的循环,这正是人类解决问题的方式。我在做一个客服问答 Agent 时感受特别明显:用户问“我的订单什么时候到”,模型先查订单状态,发现订单已签收,于是不再回答物流时间,而是转向“提供签收信息和售后入口”。如果模型一开始就凭记忆回答“预计三天后送达”,那就是灾难。
论文作者做过对照实验,ReAct 在涉及热知识更新、多步推理、事实核查等任务上,效果明显优于单次 CoT 和单纯行动。原因不复杂:CoT 容易产生幻觉,因为它无处验证;纯行动模板容易盲目调工具,因为它没有推理指引。ReAct 把两者互补,既控制了推理方向,又引入真实反馈,所以更稳。
2. 从设计到落地:ReAct 智能体的核心实现细节
2.1 Prompt 模板设计:一台“思考机器”的说明书
要把大模型调教成一个 ReAct 智能体,最关键的一步是设计系统提示词。说句实话,很多人做 Agent 效果差,根子不在模型,而在 Prompt 没有把角色的行为边界、输出格式、可用工具讲清楚。
一个合格的 ReAct System Prompt 至少包含四部分:角色定位、可用工具列表、输出格式约束、以及 few-shot 示例(可选但强烈建议)。我给一个现在还在用的模板骨架:
你是一个智能助手,你可以通过调用外部工具来获取信息和执行计算。 你必须严格按照如下格式输出你的思考和行动: Thought: 解释你当前的推理过程,说明下一步为什么要这么干。 Action: 你要调用的工具名称,必须从工具列表中选择。 Action Input: 工具参数,必须是 JSON 格式。 Observation: 这是一个占位符,工具返回结果后会以 Observation 开头拼接在对话里,你不要自行编造 Observation。 当你已经得到足够信息,可以回答用户问题时,使用以下格式结束: Thought: 我现在可以回答用户的问题了。 Final Answer: 对用户问题的最终回答。 可用工具: search: 搜索引擎,适合查询实时信息、新闻、百科知识。参数:{"query": "搜索关键词"} calculator: 计算器,适合做数学运算。参数:{"expr": "数学表达式"} get_time: 获取当前日期时间。参数:{}你注意上面这个 Prompt 里有个陷阱:我告诉模型“Observation 是占位符,不要自行编造”。这个约束极其重要。模型在生成文本时有一种“续写强迫症”,你让它看到 Thought 和 Action 之后,它常常忍不住把 Observation 也一起写了,而且写出来的还是它自己脑补的结果。如果你不提前按住它,后面整个循环的反馈就变成了“模型自己给自己喂假信息”,基本等于白干。
关于输出格式,业界有两条路线。一条是上面这种纯文本格式,用“Action:”和“Action Input:”标记,对模型要求低,兼容性好,但解析时需要写正则或字符串处理。另一条是让模型输出 JSON,比如{"thought": "...", "action": "search", "action_input": {"query": "..."}},解析方便,但有些模型在 JSON 格式下推理能力会略微下降,而且更容易输出格式错误。我的习惯是:追求稳定优先选文本格式 + 健壮解析;团队协作、下游系统对接多时选 JSON。这个没有绝对优劣,自己能 hold 住的就是好方案。
2.2 动作解析与工具调用:让模型真的能“动手”
Prompt 写好之后,你面对的第一个工程问题就是:怎么把模型输出的一串文字变成真正调用工具的动作?
我常用的解析流程分三步:先检查有没有 Final Answer;如果有,直接提取答案走人;如果没有,用正则从文本块里提取 Action 工具名和 Action Input 参数;提取到之后,把参数字符串解析成字典,再分发到对应的工具函数。
这里有几个容易翻车的小细节。Action Input 如果是 JSON,模型有时会把它包在 Markdown 代码块里,或者多出前后逗号。所以解析时我通常先做一次清理:去掉首尾空白、去掉三反引号、去掉多余换行,再做json.loads;如果解析失败,不要直接报错,而是把原始文本返回给模型一条错误消息:“你的 Action Input 格式不是合法 JSON,请重新输出。”大模型有自我纠错能力,把它拉回正轨通常是最省事的修复方式。
工具调用的架构我也不建议搞太复杂。最简单可靠的是注册表模式:一个字典,key 是工具名,value 是工具函数;再维护一个工具描述列表,自动拼进 Prompt。这样你新增工具时只需写一个新函数、加一条描述,其他逻辑都不用动。
def call_tool(name: str, args: dict) -> str: if name not in TOOL_FUNCS: return f"错误:不存在名为 {name} 的工具,请从 {list(TOOL_FUNCS.keys())} 中选择" try: result = TOOL_FUNCS[name](**args) return result except TypeError as e: return f"工具参数错误: {e}。请检查参数名和类型" except Exception as e: return f"工具执行异常: {e}"把异常也用文字形式返回给模型,而不是让 Python 直接抛错中断进程。因为 ReAct 的建模世界里,错误也是一种 Observation,模型看到了错误可以自我修正。这个思维贯穿整个 Agent 工程:你的目标不是让流程一次跑通,而是让模型在真实反馈中学会修正。
2.3 记忆与状态维护:别让模型“失忆”
ReAct 循环跑起来之后,第二个工程问题接踵而至:模型怎么记住前面几轮干了什么?
如果你只是每次把最新一条观察结果发给模型,它会立刻失忆,最后答非所问。正确做法是把整段轨迹完整地拼进消息列表里:系统消息 + 用户原始问题 + 模型输出的 Thought/Action + 系统返回的 Observation + 模型下一次的 Thought/Action ... 这串对话历史就是 Agent 的全部工作记忆。
但这里有一个资源瓶颈:聊不了几轮,上下文窗口就满了。我常用的策略是按优先级裁剪。最前面是系统消息和用户原始问题,这两部分尽量不动;中间是历史轨迹,可以压缩,比如保留最近三轮完整的 Thought/Action/Observation,更早的每条 Observation 只留一句话摘要;工具返回特别长的内容(比如搜索出一大堆网页摘要)必须主动截断,我一般限制在 2000 字符左右,并且要求工具端“只返回最相关信息”,而不是把原始 HTML 倒给模型。
另外一个实战心得:不要让模型自己维护“状态变量”。比如你让它“记住订单号是 12345”,它会口头上说“记住了”,但下一轮可能就忘了。正确的做法是你把订单号提取出来后放到外部变量里,在下一轮 Prompt 的上下文中主动注入。把状态外置,永远比期待模型自律可靠。
3. 实操一把梭:用 Python 从零搭一个最小 ReAct Agent
3.1 环境准备与工具集设计
理论聊再多,不如跑一个真实 Demo。下面这个最小实现我尽量保持精简,但你把它扩展成生产级代码也不难。依赖只有一个 openai 库(如果你用的是 OpenAI 兼容接口,还可以换成别的),或者你干脆用一个模拟的 LLM 返回函数来测试循环结构。我建议先把代码整体跑通,再接入真实模型,这样排错容易。
先定义工具集。我选三个典型工具:search(模拟搜索)、calculator(安全计算)、get_time(获取当前时间)。注意我这里演示用的 search 是 mock 版本,目的是让你不看网络波动也能复现整个流程。生产环境中你把它替换成真实的搜索 API 即可。
import json import re from datetime import datetime def mock_search(query: str) -> str: # 演示用模拟数据:凡是涉及圆周率的搜索,都返回前20位数字 if "圆周率" in query: return "圆周率前20位数字是:31415926535897932384" return f"抱歉,没有找到与 {query} 相关的信息。" def safe_calc(expr: str) -> str: allowed = set("0123456789+-*/(). ") if any(c not in allowed for c in expr): raise ValueError("表达式包含非法字符") result = eval(expr, {"__builtins__": {}}, {}) return str(result) def get_time() -> str: return datetime.now().strftime("%Y-%m-%d %H:%M:%S") TOOL_FUNCS = { "search": mock_search, "calculator": safe_calc, "get_time": get_time, }这里safe_calc我做了字符白名单过滤,并用空__builtins__限制 eval 的上下文。演示够用,但生产环境我还是建议你用更严谨的表达式解析库,比如asteval或sympy的三方解析器,因为 eval 即使写了过滤器也总有绕过的风险。这点务必上心。
3.2 核心循环代码实现
接下来是 Agent 主体。为了不让你因为缺 API key 跑不起来,我设计了一个call_llm函数,先用模拟输出测试。接入真实模型时,你只需要把这个函数内部换成真实的 API 请求。
def call_llm(messages): # 模拟模型返回,固定套路:先搜索,再计算,最后给答案 user_content = messages[-1]["content"] if messages[-1]["role"] == "user" else "" if "圆周率" in user_content and "Action" not in user_content: return "Thought: 我需要先查询圆周率前20位数字,使用 search 工具。\nAction: search\nAction Input: {\"query\": \"圆周率前20位数字\"}" if "Observation" in user_content and "calculator" not in user_content: return "Thought: 我已经获得圆周率前20位数字,现在需要计算其中奇数的和,使用 calculator 工具。\nAction: calculator\nAction Input: {\"expr\": \"3+1+5+9+5+7+9+3\"}" return "Thought: 我现在可以回答用户的问题了。\nFinal Answer: 圆周率前20位数字为 31415926535897932384,其中数字为3、1、5、9、5、7、9、3的奇数之和是 42。"这个模拟函数故意写得很傻,但它能帮你清晰看到循环结构。真实场景中你会把 LLM 的返回结构做解析,代码逻辑是通用的。现在写主循环:
AGENT_PROMPT = """你是一个智能助手,你可以通过调用外部工具来获取信息和执行计算。 ... (内容见本文第2.1节的模板)""" def parse_action(text: str): """从模型输出中提取 Action 和 Action Input""" action_match = re.search(r"Action:\s*(\w+)", text) input_match = re.search(r"Action Input:\s*(\{.*?\}|.+)", text, re.S) if not action_match or not input_match: return None, None action = action_match.group(1).strip() raw_input = input_match.group(1).strip() raw_input = raw_input.strip("`") try: action_input = json.loads(raw_input) except json.JSONDecodeError: action_input = {"query": raw_input} return action, action_input def run_agent(user_question: str, max_steps: int = 8): messages = [ {"role": "system", "content": AGENT_PROMPT}, {"role": "user", "content": user_question}, ] for step in range(max_steps): reply = call_llm(messages) # 真实场景替换这里 print(f"\n=== 第 {step+1} 轮模型输出 ===") print(reply) messages.append({"role": "assistant", "content": reply}) if "Final Answer" in reply: final = reply.split("Final Answer:")[-1].strip() print(f"\n最终答案:{final}") return final action, action_input = parse_action(reply) if not action: return "模型输出缺少 Action,终止" observation = call_tool(action, action_input) print(f"Observation: {observation}") messages.append({"role": "user", "content": f"Observation: {observation}"}) return "达到最大迭代次数,未完成" if __name__ == "__main__": run_agent("请告诉我圆周率前20位数字,并计算其中奇数的和。")你跑一下这个脚本,能看到完整的 Thought → Action → Observation 循环。这就是 ReAct 最核心的全部内容:一个 while 循环里维护消息历史,解析动作,执行工具,把真实反馈塞回去。
3.3 跑一个真实 Case:多步工具调用全过程
把上面的call_llm换成真实大模型后,我拿一个典型问题做过测试:“查询圆周率前 20 位数字,然后计算其中奇数的和。”模型实际输出的轨迹大致如下:
第一轮输出:Thought: 我需要先查询圆周率前20位数字,使用 search 工具。Action: search,Action Input: {"query": "圆周率前20位"},工具返回31415926535897932384后拼进消息历史。第二轮输出:Thought: 我已经拿到数字序列,奇数的各位是 3、1、5、9、5、7、9、3,我使用 calculator 计算它们的和。Action: calculator,Action Input: {"expr": "3+1+5+9+5+7+9+3"}。工具返回42。第三轮输出:Thought: 我已经得到计算结果,现在可以回答。Final Answer: ...。
这个过程看起来顺理成章,但你可能没意识到模型在背后做了多少决策:它判断问题里有两个子任务,先决定搜索而不是乱猜,然后又决定不用搜索而是用计算器,并且从一串数字里准确挑出奇数位。每一个决策都是在上一轮 Observation 反馈下做出的。如果你用纯 CoT 模式,模型大概率会报出一串编造的数字;如果你用纯行动模式,它可能上来就调 calculator 算一个不存在的表达式。ReAct 的真正价值就在这个“反馈修正”的循环里。
4. 常见问题与排查技巧实录
4.1 模型不按格式输出,动作解析经常失败
这是所有 ReAct 项目遇到的第一个拦路虎。你满怀期待地调用模型,结果它给你回了一大段话,里面有“Action:搜索圆周率”(中文冒号)、没有 Action Input、或者把工具名写成“搜索引擎”。我遇到最离谱的一次,模型把整个工具调用写成了一篇小作文。
解决思路也不神秘。第一,在任何解析逻辑之前,先判定“如果这一步解析失败,把错误提示注入给模型,让它重新输出”,这是自动纠错的基础。第二,在 System Prompt 里加入 1-2 条完整示例,比你说一百句“必须严格按格式输出”都管用。大模型的指令遵循能力在 few-shot 场景下会显著增强。第三,在解析时宽容一点:冒号支持中英文、工具名支持模糊匹配、Action Input 不是 JSON 时就把它当纯文本参数。宽容解析能显著提高成功率。
如果模型实在顽固,理性做法是升级格式约束层:要求它一次只输出 JSON,并且用 JSON Schema 校验。不过这会引入新的失败点,在模型评测时你要留意是否值得。
4.2 Agent 陷进死循环,工具调用停不下来
一个很经典的失败现场:模型反复调用同一工具、同一参数,实验结果完全一样,但它就是停不下来,直到耗尽 max_steps 报错。这个“复读机”问题很常见,根因是模型在长对话中丢失了“动作焦点”,或者它根本不知道接下来该干嘛了,只能机械重复上一个成功的动作。
我的处理手段有几个。第一,设置全局最大轮数硬限制,比如 10 步,这是保底。第二,发现模型连续多轮调用同一个工具且参数相同,就在 Observation 或者系统消息里加一句“你刚刚已经查过这个问题,结果没有变化。请换个思路”。这相当于人为打断复读机。第三,Prompt 里加上策略引导:“如果你发现一个行动没有带来新的信息,你应该反思并改变策略,而不是重复它。”这些听起来简单,但真的能大幅减少死循环概率。
更普适的经验是:Agent 的“退场机制”和“入场机制”同样重要。很多开发者的注意力全在如何让模型调用工具,却忘记设计如何让它尽快得出 Final Answer。你可以显式要求在最后回答前加一句“我已经获得了足够的信息”,一旦出现这个信号,优先提取 Final Answer。这样既能减少空转,也能防止模型在已经能回答时继续瞎折腾。
4.3 “白屏”问题:Agent 一句话都不输出怎么办
看到这个概念,搜过 React Native 的人可能会想起“启动白屏”。ReAct Agent 也会出现类似现象:模型调用之后没有任何可解析的 Thought、Action 或 Final Answer,整个流程像卡死一样。我排查这种“白屏”的经验按顺序来。
第一步看上下文是否爆掉。如果对话历史太长,模型直接忽略输出格式约束,或者 API 返回了截断文本,解析自然失败。第二步看解析器是否有吞内容的情况,比如正则贪婪匹配把 Action 截断了。第三步看 API 层面是否有异常,超时、限流、内容审核都有可能让返回为空。
我的统一解法是设计“超时兜底”:如果一轮模型输出解析失败,不要立即终止,而是把“你刚才的回复不符合要求,请按格式重新输出”塞回去,重试 1-2 次;重试仍失败,再记录日志并人工介入。这套兜底机制在实际项目中把成功率从 70% 拉到了 90% 以上。日志记录在这里特别重要,你至少要知道在哪个环节失败的,而不是只看到一个“白屏”。
4.4 幻觉与现实摩擦:模型编造 Observation
这是 ReAct 模式最隐蔽的坑。当模型输出 Thought 和 Action 后,由于续写惯性,它可能直接把 Observation 也写出来了:“Observation:查询结果显示圆周率前20位是314159...”——但这是模型自己编的,不是你工具返回的。如果你不加甄别地把它当成工具结果塞回去,整个 Agent 就进入了自我欺骗。
为什么会有这种问题?因为训练数据里有很多 ReAct 式的文本,模型已经学会了这种续写模式。解决办法我在第 2.1 节的 Prompt 里提过:明确告诉它“Observation 会由外部系统提供,你不得自己生成”。即便如此,代码层也要有防线:我通常的做法是把模型输出里“Observation:”后面的文本切掉,只保留到 Action Input 为止;然后把真正工具返回值以Observation: ...的完整格式重新构造消息。这样哪怕模型写了幻觉 Observation,也不会进入下一轮输入。
另一个纪律是:工具返回的内容必须是可追溯的原始内容。我在生产环境里习惯给每条 Observation 加来源前缀,比如Observation [source=search_api],模型看到带标签的内容,也会更有依据地引用。这不是必须的,但日志排查时极其舒服。
4.5 常见问题速查表
为了方便你直接检索,我把高频问题整理成一张速查表:
| 现象 | 最常见根因 | 优先处理方案 |
|---|---|---|
| 模型输出不按格式 | Prompt 约束不足 | 增加 few-shot 示例 + 格式纠错重试 |
| 反复调用同一工具 | 模型失去策略专注力 | 最大轮数 + 重复动作打断 + 策略引导 |
| 输出为空或不可解析 | 上下文超长 / API 异常 | 截断历史 + 兜底重试 + 日志追踪 |
| 编造 Observation | 模型续写惯性 | Prompt 禁止 + 代码层切掉幻觉内容 |
| 工具参数报错 | 模型对参数格式臆测 | 工具描述写明参数 + 错误信息返回给模型 |
| 最终答案质量差 | 工具结果截断过狠 | 调整截断长度 + 工具端精简返回 |
这张表基本覆盖了我在多个 Agent 项目里遇到的最常见问题。如果你按表里的方案排查还没有解决,建议把单轮完整对话轨迹打出来,人工看一遍 Thought 和 Observation 的衔接处,问题往往就出现在那两个拼接点。
5. 搜索引擎里的大坑:ReAct 和 React 不是一回事
5.1 为什么你搜 ReAct 经常搜到一堆前端内容
说点务实的题外话。ReAct 这个名词和 JS 框架 React 拼写实在是太接近了,搜索结果互相污染得非常严重。你在引擎里搜“react”,看到 react 生命周期、react 画布 flowchart、react 面试题、react native 启动白屏,十有八九都是前端开发的东西。而 ReAct 模式相关的高质量资料,反而往往淹没在这些前端内容后面。
作为过来人,我的检索建议很具体:直接搜“ReAct 论文”或者“ReAct Prompting”,尽量避免只搜 react;英文搜索可以带大模型关键词,比如 “ReAct LLM agent”;如果你看的是 LangChain 文档,直接找 “create_react_agent”,那才是我们要的。搞清楚这一点能节省你大量踩坑时间。
5.2 ReAct 模式的前沿变体与生态
在 ReAct 的基础上,业界衍生出不少成熟的变体。LangChain 的 Agent 体系里内置了 ReAct 思路,你用create_react_agent配合工具列表,几行代码就能起一个能多步推理的智能体。OpenAI 的 Function Calling 本质上也是“推理 + 行动”的另一种实现,它把工具参数用结构化方式传入,相当于把 Action 从文本约定升级成了原生 API 语义。还有一派叫 Plan-and-Execute,思路是先让模型制定整体计划,再按计划逐个执行,比 ReAct 更宏观,适合长流程任务,但动态性不如 ReAct 强。
我自己对这些框架的态度是:框架只是脚手架,ReAct 的核心设计模式才是地基。你换一个框架,循环逻辑、Prompt 约束、工具注册、观察反馈这几个要素一个都跑不掉。反过来,你即使完全不用框架,用一个几十行的循环也能实现 ReAct。原理在这个领域永远是第一位的。
最后再分享一点个人经验
我从第一次跑通 ReAct 循环,到现在评估各种 Agent 系统,一个很深的体会是:ReAct 真正难的不是代码,而是你想清楚“每一步反馈从哪里来、怎么进入下一次决策”。很多人把 Agent 做成了一堆 Tools 的堆砌,却忘了反馈闭环才是灵魂。把本文第 2 章的 Prompt 细节和第 4 章的排查逻辑吃透,你构建的 Agent 就已经超过很多只停留在“能调工具”层面的团队了。
另外提醒你一句实践建议:ReAct Agent 的每一步都可能产生不可控的外部调用,在真实项目中务必加好速率限制、日志追踪和人工审核机制。先把“最小可用的思考-行动循环”跑稳,再谈复杂度。这个顺序,我走过弯路,希望你一步到位。