Core MapReduceChain token counting before combine step - Performance 

### System Info

This impacts both JS and Python versions

### Who can help?

@hwchase17 
@nfcampos 

### Information

- [X] The official example notebooks/scripts
- [X] My own modified scripts

### Related Components

- [X] LLMs/Chat Models
- [ ] Embedding Models
- [ ] Prompts / Prompt Templates / Prompt Selectors
- [ ] Output Parsers
- [ ] Document Loaders
- [ ] Vector Stores / Retrievers
- [ ] Memory
- [ ] Agents / Agent Executors
- [ ] Tools / Toolkits
- [X] Chains
- [ ] Callbacks/Tracing
- [ ] Async

### Reproduction

Discussed this with @nfcampos over slack, but pasting it here for documentation/community discussion to validate this change/issue.

I have switched from JS -> Python version but this issue is valid for both. 
When using JS version, I noticed the mapreduce chains combine step decides when it should combine based on the token count sum of the map steps entire prompt, instead of just the outputs. This presented a few issues:
- The point of the map step, as I understood it, is to condense the chunk to eventually be able to fit in the combine prompt, and so gauging when to combine based on summing all map steps’ (input+output) token counts didn’t make sense. This leads to several unnecessary iterations, and much longer run time than if it’s just the map steps’ output tokens being summed, which is what is eventually used in the combine step anyway.
- If the map prompt has large instructions, then it is possible that the sum will never reduce past the threshold (token_max default is 3000). Example:
  - You have ten chunks having the map step being run.
  - Each chunk has 400 token instructions in map Prompt. 400*10= 4000 tokens.
  - The map steps would never condense enough to drop below the 3000 token limit, even though the output from each map step might be 100 tokens. Worse still, it would run up $$$ by running map steps until max_iterations.


I can share my monkey patch of the langchain JS code if it helps, and had even proposed a PR for this in the langchianjs discord. Now that we moved to python, and since I haven’t contributed to any python projects, I thought I’d check here before proposing this change or even attempting a PR.


My questions before proceeding are - does this sound right to you? Is there any reason why a generic mapreduce chain would use the entire map prompt from the map step, and not just the outputs when deciding to combine?

### Expected behavior

The map_prompt output tokens should only be summed up when deciding if we can combine or not.

In the JS version, my monkey patched langchain MapReduce chain's implementation looked like:

```
async _call(values) {
    if (!(this.inputKey in values)) {
      throw new Error(`Document key ${this.inputKey} not found.`);
    }
    const { [this.inputKey]: docs, ...rest } = values;
    let currentDocs = docs;
    let totalIterations = 0;
    for (let i = 0; i < this.maxIterations; i += 1) {
      const inputs = currentDocs.map((d) => ({
        [this.documentVariableName]: d.pageContent,
        ...rest,
      }));
      const promises = inputs.map(async (input) => {
        const prompt = await this.llmChain.prompt.format(input);
        return this.llmChain.llm.getNumTokens(prompt);
      });
      const length = await Promise.all(promises).then((results) =>
        results.reduce((a, b) => a + b, 0)
      );
      const joinedInputs = {
        [this.documentVariableName]: inputs
          .map((_) => _[this.documentVariableName])
          .join('\n\n'),
        ...rest,
      };
      // Speed up converging - Patched Token Counting
      const joinedInputTextsPrompt = await this.llmChain.prompt.format(
        joinedInputs
      );
      const joinedInputTextsLength =
        (await this.llmChain.llm.getNumTokens(joinedInputTextsPrompt)) +
        (await this.llmChain.llm.getNumTokens(
          this.combineDocumentChain.llmChain.prompt.template
        ));
      console.log({
        length,
        joinedInputTextsLength,
      });
      const canSkipMapStep = i !== 0 || !this.ensureMapStep;
      // const withinTokenLimit = length < this.maxTokens; // Original implementation
      const withinTokenLimit = joinedInputTextsLength < this.maxTokens;
      if (canSkipMapStep && withinTokenLimit) {
        break;
      }
      console.time('MapReduceChain:mapStep');
      const results = await this.llmChain.apply(inputs);
      console.timeEnd('MapReduceChain:mapStep');
      totalIterations += 1;
      const { outputKey } = this.llmChain;
      currentDocs = results.map((r) => ({
        pageContent: r[outputKey],
      }));
    }
    const newInputs = { input_documents: currentDocs, ...rest };
    console.time('MapReduceChain:combineStep');
    const result = await this.combineDocumentChain.call(newInputs);
    console.timeEnd('MapReduceChain:combineStep');
    console.log('Iterations: ', totalIterations);
    return result;
  }
```

Relevant change:
```
const joinedInputTextsLength =
        (await this.llmChain.llm.getNumTokens(joinedInputTextsPrompt)) +
        (await this.llmChain.llm.getNumTokens(
          this.combineDocumentChain.llmChain.prompt.template
        ));      
// const withinTokenLimit = length < this.maxTokens; // Original implementation
const withinTokenLimit = joinedInputTextsLength < this.maxTokens;
```