Llamadas a herramientas (también conocidas como llamadas a funciones)
Aprende cómo implementar la llamada a herramientas, gestionar bucles agénticos e incorporar interacciones con intervención humana (human-in-the-loop) usando el Firebase AI Logic SDK.
Aunque es cierto que los LLM están entrenados esencialmente en todo el internet, no lo saben todo. Saben lo que estaba en el internet público el día en que fueron entrenados, pero no saben nada más reciente que eso. No saben nada que sea privado para ti o para tu organización. E incluso las cosas que sí saben pueden enredarse fácilmente con otras cosas que saben.
Para esos escenarios, y muchos otros, a menudo le proporcionamos a un LLM una o más herramientas.
Herramienta definida
#Una herramienta es un nombre, una descripción y un esquema JSON para el formato de los datos de entrada cuando el LLM "llama" a la herramienta. Por ejemplo, si le indicamos al LLM con un prompt "Reduce los carbohidratos en la receta de desayuno All American de la abuela", no sabrá cuál es la receta de la abuela a menos que le demos una herramienta "lookupRecipe" que reciba una cadena de consulta que podamos usar para buscar la receta.
Conceptualmente, una herramienta es algo que le damos al LLM para que llame cuando necesite esos datos o servicios. La forma en que un LLM llama a una herramienta es respondiendo a la solicitud de la aplicación con un mensaje especialmente formateado que significa "llamada a herramienta" (tool call). Un mensaje de llamada a herramienta incluye el nombre y los argumentos JSON para la herramienta. La aplicación maneja la llamada a la herramienta y agrupa el resultado en otra solicitud de LLM, a la cual el LLM responde luego.
Esto puede continuar por un tiempo. Una aplicación puede configurar una instancia de modelo con cualquier número de herramientas (aunque al LLM le va mejor con un conjunto más pequeño de herramientas específicas que no dupliquen su funcionalidad). El LLM puede agrupar cualquier número de llamadas a herramientas en su respuesta y puede tomar cualquier número de resultados de herramientas en una solicitud. El LLM consolida múltiples viajes de ida y vuelta para prompts y resultados de llamadas a herramientas a través de una pila de mensajes que forman un historial de pares de solicitud/respuesta.
Cuando termina con las llamadas a herramientas, el LLM devuelve su respuesta final, por ejemplo "Aquí tienes una versión de la receta de desayuno All American de la abuela que es alta en proteínas y baja en carbohidratos...".
Funciones de Gemini
#En el Firebase AI Logic SDK, una herramienta se llama "función", pero es lo mismo. En la muestra, el modelo de resolución de pistas está configurado con una función para buscar detalles de palabras. Si el LLM desea detalles sobre una palabra para ayudar con el proceso de resolución, llamar a la función le proporciona datos de la Free Dictionary API:
[
{
"word": "tool",
"phonetic": "/tuːl/",
"phonetics": [
{
"text": "/tuːl/",
"audio": "https://api.dictionaryapi.dev/media/pronunciations/en/tool-uk.mp3",
"sourceUrl": "https://commons.wikimedia.org/w/index.php?curid=94709459",
"license": {
"name": "BY-SA 4.0",
"url": "https://creativecommons.org/licenses/by-sa/4.0"
}
}
],
"meanings": [
{
"partOfSpeech": "noun",
"definitions": [
{
"definition": "A mechanical device intended to make a task easier.",
"synonyms": [],
"antonyms": [],
"example": "Hand me that tool, would you? I don't have the right tools to start fiddling around with the engine."
},
...
La aplicación tiene una función Dart que realiza la búsqueda:
// Look up the metadata for a word in the dictionary API.
Future<Map<String, dynamic>> _getWordMetadataFromApi(String word) async {
final url = Uri.parse(
'https://api.dictionaryapi.dev/api/v2/entries/en/${Uri.encodeComponent(word)}',
);
final response = await http.get(url);
return response.statusCode == 200
? {'result': jsonDecode(response.body)}
: {'error': 'Could not find a definition for "$word".'};
}
El modelo se configura con la función de búsqueda como parte de la inicialización:
// The model for solving clues.
_clueSolverModel = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.5-flash',
systemInstruction: Content.text(clueSolverSystemInstruction),
tools: [
Tool.functionDeclarations([
FunctionDeclaration(
'getWordMetadata',
'Gets grammatical metadata for a word, like its part of speech. '
'Best used to verify a candidate answer against a clue that implies a '
'grammatical constraint.',
parameters: {
'word': Schema(SchemaType.string, description: 'The word to look up.'),
},
),
]),
],
);
Para mayor confiabilidad, también es una buena idea listar las herramientas en la instrucción del sistema:
static String get clueSolverSystemInstruction =>
'''
You are an expert crossword puzzle solver.
...
### Tool: `getWordMetadata`
You have a tool to get grammatical information about a word.
**When to use:**
- This tool is most helpful as a verification step after you have a likely answer.
- Consider using this tool when a clue contains a grammatical hint that could be ambiguous.
- **Good candidates for verification:**
- Clues that seem to be verbs (e.g., "To run," "Waving").
- Clues that are adverbs (e.g., "Happily," "Quickly").
- Clues that specify a plural form.
- **Try to avoid using the tool for:**
- Simple definitions (e.g., "A small dog").
- Fill-in-the-blank clues (e.g., "___ and flow").
- Proper nouns (e.g., "Capital of France").
**Function signature:**
```json
${jsonEncode(_getWordMetadataFunction.toJson())}
```
''';
Cuando la aplicación realiza una solicitud, el modelo ahora tiene una herramienta para usar cuando decida que le será de ayuda. Para admitir llamadas a herramientas, necesitamos implementar un bucle agéntico.
El bucle agéntico
#Un LLM es funcionalmente sin estado (stateless), lo que significa que tienes que darle todos
los datos que necesita con cada solicitud. Para una solicitud que es solo el prompt y
cualquier archivo que desees enviar, el Firebase AI Logic SDK expone el
método generateContent en la instancia de tu modelo.
Sin embargo, la llamada a herramientas requiere un historial de mensajes que formen el prompt inicial, así como los pares de respuesta/solicitud que componen las llamadas a herramientas y los resultados de las herramientas. Para admitir esto, Firebase Logic AI proporciona un objeto "chat" para recopilar el historial. Lo usamos para construir el bucle agéntico:
- Iniciar un chat para mantener el historial de mensajes a través de múltiples pares de solicitud/respuesta
- Recopilar los resultados de las herramientas para cualquier llamada a herramienta que proporcione
- Agrupar los resultados de las herramientas en una nueva solicitud
- Realizar un bucle hasta que el modelo proporcione una respuesta sin llamadas a herramientas
- Devolver el texto acumulado a través de todas las respuestas
Aquí está ese algoritmo expresado como un método de extensión en la clase GenerativeModel de modo que podamos llamarlo igual que llamamos a
generateContent:
extension on GenerativeModel {
Future<String> generateContentWithFunctions({
required String prompt,
required Future<Map<String, dynamic>> Function(FunctionCall) onFunctionCall,
}) async {
// Use a chat session to support multiple request/response pairs, which is
// needed to support function calls.
final chat = startChat();
final buffer = StringBuffer();
var response = await chat.sendMessage(Content.text(prompt));
while (true) {
// Append the response text to the buffer.
buffer.write(response.text ?? '');
// If no function calls were collected, we're done
if (response.functionCalls.isEmpty) break;
// Append a newline to separate responses.
buffer.write('\n');
// Execute all function calls
final functionResponses = <FunctionResponse>[];
for (final functionCall in response.functionCalls) {
try {
functionResponses.add(
FunctionResponse(
functionCall.name,
await onFunctionCall(functionCall),
),
);
} catch (ex) {
functionResponses.add(
FunctionResponse(functionCall.name, {'error': ex.toString()}),
);
}
}
// Get the next response stream with function results
response = await chat.sendMessage(
Content.functionResponses(functionResponses),
);
}
return buffer.toString();
}
}
Este método toma un prompt y un callback para manejar las llamadas a herramientas específicas, el cual la muestra llama para manejar la función de búsqueda de palabras:
await _clueSolverModel.generateContentWithFunctions(
prompt: getSolverPrompt(clue, length, pattern),
onFunctionCall: (functionCall) async => switch (functionCall.name) {
'getWordMetadata' => await _getWordMetadataFromApi(
functionCall.args['word'] as String,
),
_ => throw Exception('Unknown function call: ${functionCall.name}'),
},
);
La salida estructurada hace que un LLM sea útil para programar, pero son las herramientas las que convierten a un LLM en un "agente" (más sobre esto en la sección Modo de interacción).
Salida estructurada y llamadas a herramientas
#La combinación de salida estructurada y llamadas a herramientas produce una combinación potente. En la muestra, el resolvedor de pistas tiene una herramienta para buscar detalles de palabras. También se le pide que devuelva un JSON que agrupe la solución con una puntuación de confianza, y ambos se muestran en la lista de tareas de la aplicación:
Desafortunadamente, al momento de escribir esto, combinar salidas estructuradas y funciones al usar el Firebase AI Logic SDK produce una excepción:
Function calling with a response mime type: 'application/json' is unsupported
Como una solución alternativa (con suerte temporal) para este problema, la muestra elimina la
configuración de salida estructurada, utilizando en su lugar una herramienta llamada returnResult
para
simular la salida estructurada:
// The model for solving clues.
_clueSolverModel = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.5-flash',
systemInstruction: Content.text(clueSolverSystemInstruction),
tools: [
Tool.functionDeclarations([
...,
FunctionDeclaration(
'returnResult',
'Returns the final result of the clue solving process.',
parameters: {
'answer': Schema(
SchemaType.string,
description: 'The answer to the clue.',
),
'confidence': Schema(
SchemaType.number,
description: 'The confidence score in the answer from 0.0 to 1.0.',
),
},
),
]),
],
);
El método returnResult también se menciona en la instrucción del sistema:
static String get clueSolverSystemInstruction =>
'''
You are an expert crossword puzzle solver.
...
### Tool: `returnResult`
You have a tool to return the final result of the clue solving process.
**When to use:**
- Use this tool when you have a final answer and confidence score to return. You
must use this tool exactly once, and only once, to return the final result.
**Function signature:**
```json
${jsonEncode(_returnResultFunction.toJson())}
```
''';
Cuando el modelo llama a returnResult, la muestra almacena en caché el resultado, el cual
solveClue busca después de llamar a generateContentWithFunctions:
// Buffer for the result of the clue solving process.
final _returnResult = <String, dynamic>{};
// Cache the return result of the clue solving process via a function call.
// This is how we get JSON responses from the model with functions, since the
// model cannot return JSON directly when tools are used.
Map<String, dynamic> _cacheReturnResult(Map<String, dynamic> returnResult) {
assert(_returnResult.isEmpty);
_returnResult.addAll(returnResult);
return {'status': 'success'};
}
Future<ClueAnswer?> solveClue(Clue clue, int length, String pattern) async {
// Clear the return result cache; this is where the result will be stored.
_returnResult.clear();
// Generate JSON response with functions and schema.
await _clueSolverModel.generateContentWithFunctions(
prompt: getSolverPrompt(clue, length, pattern),
onFunctionCall: (functionCall) async => switch (functionCall.name) {
'getWordMetadata' => ...,
'returnResult' => _cacheReturnResult(functionCall.args),
_ => throw Exception('Unknown function call: ${functionCall.name}'),
},
);
// Use the structured output that the LLM has called function with
assert(_returnResult.isNotEmpty);
return ClueAnswer(
answer: _returnResult['answer'] as String,
confidence: (_returnResult['confidence'] as num).toDouble(),
);
}
Tenemos que trabajar un poco más para obtener la combinación de salida estructurada y llamadas a herramientas utilizando Firebase AI Logic, ¡pero los resultados valen la pena!
Intervención humana
#Hasta ahora, hemos visto herramientas utilizadas para recopilar datos y formatear salidas. También podemos usarlas para involucrar a un ser humano.
Como ejemplo, a veces cuando la muestra le pasa un patrón que la solución
debería tomar – como "_R_Y" – el modelo quiere sugerir una respuesta que no
encaja con este patrón – como "RENT". Un conflicto como este es un buen momento para pedir
ayuda al usuario:
Esto se llama poner al "humano en el bucle" (human in the loop) y es otra forma en que
los humanos y los LLM pueden colaborar. Flutter y el Firebase AI Logic SDK hacen que esto
sea fácil de hacer. Primero, el ejemplo define una función y configura el modelo:
// The new function to let the LLM resolve solution conflicts
static final _resolveConflictFunction = FunctionDeclaration(
'resolveConflict',
'Asks the user to resolve a conflict between the letter pattern and the '
'proposed answer. Use this BEFORE calling returnResult if the answer you '
'want to propose does not match the letter pattern.',
parameters: {
'proposedAnswer': Schema(
SchemaType.string,
description: 'The answer the LLM wants to suggest.',
),
'pattern': Schema(
SchemaType.string,
description: 'The current letter pattern from the grid.',
),
'clue': Schema(SchemaType.string, description: 'The clue text.'),
},
);
// Pass the new tool to the model for solving clues.
final _clueSolverModel = FirebaseAI.googleAI().generativeModel(
model: 'gemini-2.5-flash',
systemInstruction: Content.text(clueSolverSystemInstruction),
tools: [
Tool.functionDeclarations([
...
_resolveConflictFunction,
]),
],
);
// Let the LLM know that it has a new tool.
static String get clueSolverSystemInstruction =>
'''
You are an expert crossword puzzle solver.
...
### Tool: `resolveConflict`
You have a tool to ask the user to resolve a conflict.
**When to use:**
- Use this tool **BEFORE** `returnResult` if your proposed answer conflicts with the provided letter pattern.
- For example, if the pattern is `_ R _ Y` and you want to suggest `RENT` (which fits the clue), there is a conflict at the second letter (`R` vs `E`). You should call `resolveConflict(proposedAnswer: "RENT", pattern: "_ R _ Y", clue: "...")`.
- The tool will return the user's decision (either your proposed answer or a new one). You should then use that result to call `returnResult`.
**Function signature:**
```json
${jsonEncode(_resolveConflictFunction.toJson())}
```
''';
Ahora cuando el modelo vea un conflicto, llamará a la herramienta:
// handle the LLM's request to resolve the conflict
await _clueSolverModel.generateContentWithFunctions(
prompt: getSolverPrompt(clue, length, pattern),
onFunctionCall: (functionCall) async => switch (functionCall.name) {
...
'resolveConflict' => await _handleResolveConflict(
functionCall.args,
onConflict,
),
},
);
// Show the dialog to gather the user's input
Future<Map<String, dynamic>> _handleResolveConflict(
Map<String, dynamic> args,
Future<String> Function(String clue, String proposedAnswer, String pattern)?
onConflict,
) async {
final proposedAnswer = args['proposedAnswer'] as String;
final pattern = args['pattern'] as String;
final clue = args['clue'] as String;
if (onConflict != null) {
final result = await onConflict(clue, proposedAnswer, pattern);
return {'result': result};
}
return {'result': proposedAnswer};
}
La muestra maneja la herramienta con una implementación del método onConflict
que llama a showDialog para recopilar datos del usuario. Todo esto sucede en el
medio del bucle agéntico, pero está bien; el modelo no está esperando; ya ha
enviado de vuelta su respuesta a la solicitud inicial de la aplicación. El usuario puede tomarse
su tiempo con la interfaz de usuario mientras la muestra espera el Future devuelto por
showDialog. Cuando termina, el modelo continúa donde lo dejó utilizando el
historial de mensajes y la solicitud más reciente, que en este caso resulta ser los
datos recopilados de forma interactiva del usuario.
Un cuadro de diálogo modal es una forma sencilla de poner al humano en el ciclo, pero no es la
única forma de hacerlo en Flutter. Si lo prefieres, una instancia de un
Completer te permite establecer algún estado en tu aplicación que la ponga en
modo "recopilando datos del usuario". Cuando la aplicación tiene los datos, puede llamar a
complete en el Completer y reanudar el bucle agéntico.
O bien, dado que eres el propietario del bucle agéntico, puedes verificar si hay una llamada a una función "especial" que indique que necesitas recopilar datos del usuario. Este tipo de función especial a veces se denomina "interrupción" (interrupt) y "reasumes" la conversión con el modelo cuando tienes los datos del usuario.
Recuerda que el LLM no tiene estado. No te está esperando, por lo que puedes manejar el bucle agéntico de la manera que tenga más sentido para tu aplicación. Puedes volver al LLM con un historial de mensajes actualizado y un nuevo prompt en cualquier momento, ya sea que haya pasado un minuto o un mes.
A menos que se indique lo contrario, la documentación de este sitio refleja Flutter 3.44.0. Página actualizada por última vez el 2026-05-05. Ver código fuente oreportar un problema.